diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 7750ba32..00000000
--- a/.coveragerc
+++ /dev/null
@@ -1,12 +0,0 @@
-[report]
-fail_under = 80.0
-omit =
- **/deployment/*
- **/__init__.py
- setup.py
- **/tests/*
- source/infrastructure/app.py
- source/backstage/cdk/source/infrastructure/app.py
- **/*_dependency_layer/**/*
- **/*_dep_layer/**/*
- **/*-dep-layer/**/*
diff --git a/.gitignore b/.gitignore
index da7d4d85..c23955a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,20 +5,10 @@ cdk.context.json
environment_tars
*_dependency_layer
-*.js
-*postman_collection.json
-!**/postman_collection/index.js
-!deployment/.typescript/cdk-solution-helper/index.js
-!jest.config.js
*.d.ts
node_modules
-!source/.typescript/lambda/**/*.js
-!source/lambda/**/*.js
coverage/
-# Test script runtime configuration
-templates/modules/cms_provisioning_on_aws/**/vehicle_config.json
-
# Certification files
# In case you have these in your directory for deployment/testing
*.pem
@@ -30,8 +20,6 @@ templates/modules/cms_provisioning_on_aws/**/vehicle_config.json
cdk.out
chalice.out
-!**/cdk-solution-helper/index.js
-
staging
global-s3-assets
regional-s3-assets
@@ -50,8 +38,6 @@ temp/
### macOS ###
.DS_Store
-coverage-reports/
-
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -65,6 +51,7 @@ __pycache__/
build/
develop-eggs/
dist/
+dist-lib/
downloads/
eggs/
.eggs/
@@ -98,6 +85,7 @@ htmlcov/
.cache
nosetests.xml
coverage.xml
+coverage-reports/
*.cover
*.py,cover
.hypothesis/
@@ -105,6 +93,7 @@ coverage.xml
cover/
*-cfnlogs.txt
.nightswatch/functional/results
+.cdk_cache
# Translations
*.mo
@@ -137,45 +126,11 @@ target/
profile_default/
ipython_config.py
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
-# pdm
-# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-#pdm.lock
-# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
-# in version control.
-# https://pdm.fming.dev/#use-with-ide
-#.pdm.toml
-
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
# Environments
-.env*
+*.cmsrc
.venv
env/
venv/
@@ -183,15 +138,9 @@ ENV/
env.bak/
venv.bak/
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
# mkdocs documentation
-/site
+**/.acdp/docs
+**/.acdp/site
# mypy
.mypy_cache/
@@ -209,11 +158,6 @@ cython_debug/
# PyCharm
.idea
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
#draw.io backup files
**/*.xml.bkp
diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index d5de0953..00000000
--- a/.isort.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-[settings]
-src_paths = **/cms-connect-store-on-aws,**/cms-provisioning-on-aws
-known_third_party = arrow,attr,attrs,aws_cdk,aws_lambda_powertools,aws_secretsmanager_caching,aws_solutions_constructs,awscrt,awsiot,backoff,boto3,botocore,cattrs,cdk_nag,chalice,constructs,cryptography,dataclass_type_validator,dateutil,freezegun,grafanalib,jinja2,jose,jsii,markdown_to_json,moto,mypy_boto3_dynamodb,mypy_boto3_iot,mypy_boto3_lambda,mypy_boto3_secretsmanager,pytest,requests,responses,setuptools,syrupy,toml
-import_heading_stdlib=Standard Library
-import_heading_thirdparty=Third Party Libraries
-import_heading_localfolder=Connected Mobility Solution on AWS
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
index 816c57f0..f90ed06f 100644
--- a/.markdownlint.yaml
+++ b/.markdownlint.yaml
@@ -1,5 +1,2 @@
-{
- "MD013": {
- "line_length": 120
- }
-}
+MD013:
+ line_length: 120
diff --git a/.nvmrc b/.nvmrc
index 4a1f488b..aacb5181 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-18.17.1
+18.17
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c2c8d325..8d16763c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,238 +1,275 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+exclude: '^.*\.svg$'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v3.2.0
+ rev: v4.5.0
hooks:
- - id: check-byte-order-marker # Forbid UTF-8 byte-order markers
- exclude: ^(templates/|.nightswatch/)
- - id: check-case-conflict # Check for files with names that would conflict on a case-insensitive
- # filesystem like MacOS HFS+ or Windows FAT.
- exclude: ^(templates/|.nightswatch/)
+ - id: check-executables-have-shebangs
+ name: (ROOT) Check executables have shebangs
+ exclude: ^(source/|.nightswatch/)
+ - id: fix-byte-order-marker
+ name: (ROOT) Fix byte order marker
+ exclude: ^(source/|.nightswatch/)
+ - id: check-case-conflict
+ name: (ROOT) Check case conflict
+ exclude: ^(source/|.nightswatch/)
- id: check-json
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Check json
+ exclude: ^(source/|.nightswatch/)
- id: check-yaml
- exclude: (^source/backstage/examples|^.*/catalog-info.yaml|^templates)
+ name: (ROOT) Check yaml
+ exclude: ^(source/|.nightswatch/)
+ args: [--allow-multiple-documents, --unsafe]
- id: check-toml
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Check toml
+ exclude: ^(source/|.nightswatch/)
- id: check-merge-conflict
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Check for merge conflicts
+ exclude: ^(source/|.nightswatch/)
- id: check-added-large-files
+ name: (ROOT) Check for added large files
exclude: |
(?x)^(
- ^templates |
+ ^source/ |
+ ^.nightswatch/ |
^.*/package-lock.json |
^.*/yarn.lock |
^.*/Pipfile.lock
)$
- id: end-of-file-fixer
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Fix end of the files
+ exclude: ^(source/|.nightswatch/)
- id: fix-encoding-pragma
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Fix python encoding pragma
+ exclude: ^(source/|.nightswatch/)
- id: trailing-whitespace
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Trim trailing whitespace
+ exclude: ^(source/|.nightswatch/)
- id: mixed-line-ending
- exclude: ^(templates/|.nightswatch/)
- - id: sort-simple-yaml # Requires explicit files parameter to enable file matching
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Mixed line ending
+ exclude: ^(source/|.nightswatch/)
- id: detect-aws-credentials
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Detect AWS credentials
+ exclude: ^(source/|.nightswatch/)
args: ["--credentials-file", "~/.ada/credentials"]
- id: detect-private-key
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Detect private keys
+ exclude: ^(source/|.nightswatch/)
- repo: https://github.com/Lucas-C/pre-commit-hooks
- rev: v1.5.1
+ rev: v1.5.4
hooks:
- id: insert-license
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Insert license header (python)
+ exclude: ^(source/|.nightswatch/)
files: \.py$
args:
- --license-filepath
- - ./license_header.txt # defaults to: LICENSE.txt
+ - ./license_header.txt
- --detect-license-in-X-top-lines=3
- - repo: https://github.com/Lucas-C/pre-commit-hooks
- rev: v1.5.1
- hooks:
- id: insert-license
- exclude: ^(templates/|.nightswatch/)
- files: \.tsx|.ts$
+ name: (ROOT) Insert license header (typescript and javascript)
+ files: \.tsx$|\.ts$|\.js$|\.jsx$
+ exclude: ^(source/|.nightswatch/)
args:
- --license-filepath
- - ./license_header.txt # defaults to: LICENSE.txt
+ - ./license_header.txt
- --comment-style
- - // # defaults to: #
+ - // # defaults to Python's # syntax, requires changing for typescript syntax.
- --detect-license-in-X-top-lines=3
- repo: https://github.com/psf/black
- rev: 22.3.0
+ rev: 23.10.1
hooks:
- id: black
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Black
+ exclude: ^(source/|.nightswatch/)
- repo: https://github.com/hadialqattan/pycln
- rev: v2.2.2
+ rev: v2.3.0
hooks:
- id: pycln
- exclude: ^(templates/|.nightswatch/)
+ name: (ROOT) Pycln
+ exclude: ^(source/|.nightswatch/)
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
- name: isort (python)
- exclude: ^(templates/|.nightswatch/)
- args: ["--profile", "black"]
+ name: (ROOT) Isort (python)
+ exclude: ^(source/|.nightswatch/)
+ args: ["--skip-glob", "**/node_modules/* **/.venv/*", "--settings-path", "./pyproject.toml"]
- repo: https://github.com/PyCQA/bandit
- rev: 1.7.4
+ rev: 1.7.5
hooks:
- id: bandit
- exclude: ^(templates/|.nightswatch/)
- args: ["-c", "pyproject.toml"]
+ name: (ROOT) Bandit
+ exclude: ^(source/|.nightswatch/)
+ args: ["-c", "./pyproject.toml"]
additional_dependencies: [ "bandit[toml]" ]
- # - repo: https://github.com/kontrolilo/kontrolilo
- # rev: v2.2.0
- # hooks:
- # - id: license-check-configuration-lint
- # exclude: ^(templates/|.nightswatch/)
- # language: python
- # - id: license-check-pipenv
- # exclude: ^(templates/|.nightswatch/)
- # language: python
- # - id: license-check-npm
- # exclude: ^(templates/|.nightswatch/)
- # language: python
- repo: https://github.com/pypa/pip-audit
rev: v2.6.1
hooks:
- id: pip-audit
- exclude: ^(templates/|.nightswatch/)
-
- # Local hooks
- - repo: local
+ name: (ROOT) Pip audit
+ exclude: ^(source/|.nightswatch/)
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v3.1.0
hooks:
- - id: check-bash-syntax
- exclude: ^(templates/|.nightswatch/)
- name: Check Shell scripts syntax correctness
- language: system
- entry: bash -n
- files: \.sh$
+ - id: prettier
+ name: (ROOT) Prettier
+ types_or: [javascript, jsx, ts, tsx]
+ exclude: ^(source/|.nightswatch/)
+ # Local
- repo: local
hooks:
- id: detect-empty-files
- exclude: ^(templates/|.nightswatch/)
- name: Detect empty files in the repo
- entry: deployment/detect-empty-files.sh
+ name: (ROOT) detect-empty-files
+ exclude: ^(source/|.nightswatch/)
+ entry: deployment/run-detect-empty-files.sh
language: system
pass_filenames: false
- - repo: local
- hooks:
+ - id: shellcheck
+ name: (ROOT) Shellchecker
+ exclude: ^(source/|.nightswatch/)
+ entry: ./deployment/run-shellcheck.sh
+ args: ["-x"]
+ types: [shell]
+ language: system
- id: pylint
- exclude: ^(templates/|.nightswatch/)
- name: pylint
- entry: pylint
- args: ["--extension-pkg-allow-list", "math"]
+ name: (ROOT) pylint
+ exclude: ^(source/|.nightswatch/)
+ entry: pipenv run pylint
+ args: ["--extension-pkg-allow-list", "math", "--rcfile", "./pyproject.toml"]
types: [python]
language: system
require_serial: true
- - repo: local
- hooks:
- id: mypy
- exclude: ^(templates/|.nightswatch/)
- name: mypy
- entry: mypy
+ name: (ROOT) mypy
+ exclude: ^(source/|.nightswatch/)
+ entry: pipenv run mypy
types_or: [python, pyi]
- args: ["--strict", "--cache-dir", "/dev/null"]
+ args: ["--strict", "--cache-dir", "./.mypy_cache", "--config-file", "./pyproject.toml"]
language: system
require_serial: true
+ # Module pre-commits: https://github.com/pre-commit/pre-commit/issues/731#issuecomment-376945745
- repo: local
hooks:
- - id: cfn-nag
- exclude: ^(templates/|.nightswatch/)
- name: cfn-nag
- entry: deployment/run-cfn-nag.sh
- files: infrastructure
- args: ["--no-nested"]
- language: system
- types_or: [python, json]
- pass_filenames: false
- - repo: local
- hooks:
- - id: pytest-jest
- exclude: ^(templates/|.nightswatch/)
- name: pytest-jest
- entry: deployment/run-unit-tests.sh
- args: ["--no-report", "--no-nested"]
- files: (^source)
- language: system
- types_or: [python, javascript, jsx, ts, tsx]
- pass_filenames: false
-
-
- # Run module level precommit hooks https://github.com/pre-commit/pre-commit/issues/731#issuecomment-376945745
- - repo: local
- hooks:
- - id: module-alerts-hooks
- name: CMS Alerts hooks
+ - id: acdp
+ name: (ACDP)
language: script
- args: ["--module", "alerts", "--files-list"]
- entry: ./deployment/run_module_hooks.py
- files: ^templates/modules/cms_alerts_on_aws
+ args: ["--module-path", "source/modules/acdp", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/acdp
verbose: true
require_serial: true
-
- - repo: local
- hooks:
- - id: module-api-hooks
- name: CMS API hooks
+ - id: cms-alerts
+ name: (Alerts)
language: script
- args: ["--module", "api", "--files-list"]
- entry: ./deployment/run_module_hooks.py
- files: ^templates/modules/cms_api_on_aws
+ args: ["--module-path", "source/modules/cms_alerts", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/cms_alerts
verbose: true
require_serial: true
-
- - repo: local
- hooks:
- - id: module-connect-store-hooks
- name: CMS Connect & Store hooks
+ - id: cms-api
+ name: (API)
language: script
- args: ["--module", "connect_store", "--files-list"]
- entry: ./deployment/run_module_hooks.py
- files: ^templates/modules/cms_connect_store_on_aws
+ args: ["--module-path", "source/modules/cms_api", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/cms_api
verbose: true
require_serial: true
- - repo: local
- hooks:
- - id: module-ev-battery-health-hooks
- name: CMS EV Battery Health hooks
+ - id: cms-common
+ name: (Common)
language: script
- args: ["--module", "ev_battery_health", "--files-list"]
- entry: ./deployment/run_module_hooks.py
- files: ^templates/modules/cms_ev_battery_health_on_aws
+ args: ["--module-path", "source/lib", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/lib
verbose: true
require_serial: true
- - repo: local
- hooks:
- - id: module-provisioning-hooks
- name: CMS Provisioning hooks
+ - id: cms-config
+ name: (Config)
language: script
- args: ["--module", "provisioning", "--files-list"]
- entry: ./deployment/run_module_hooks.py
- files: ^templates/modules/cms_provisioning_on_aws
+ args: ["--module-path", "source/modules/cms_config", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/cms_config
verbose: true
require_serial: true
- - repo: local
- hooks:
- - id: module-user-authentication-hooks
- name: CMS User Authentication hooks
+ - id: cms-connect-store
+ name: (Connect Store)
language: script
- args: ["--module", "user_authentication", "--files-list"]
- entry: ./deployment/run_module_hooks.py
- files: ^templates/modules/cms_user_authentication_on_aws
+ args: ["--module-path", "source/modules/cms_connect_store", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/cms_connect_store
verbose: true
require_serial: true
- - repo: local
- hooks:
- - id: module-vehicle-simulator-hooks
- name: CMS Vehicle Simulator hooks
+ - id: cms-ev-battery-health
+ name: (EV Battery Health)
+ language: script
+ args: ["--module-path", "source/modules/cms_ev_battery_health", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/cms_ev_battery_health
+ verbose: true
+ require_serial: true
+ - id: cms-provisioning
+ name: (Vehicle Provisioning)
+ language: script
+ args: ["--module-path", "source/modules/cms_provisioning", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/cms_provisioning
+ verbose: true
+ require_serial: true
+ - id: cms-sample
+ name: (Sample)
+ language: script
+ args: ["--module-path", "source/modules/cms_sample", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/cms_sample
+ verbose: true
+ require_serial: true
+ - id: cms-auth
+ name: (Auth)
+ language: script
+ args: ["--module-path", "source/modules/cms_auth", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/cms_auth
+ verbose: true
+ require_serial: true
+ - id: cms-vehicle-simulator
+ name: (Vehicle Simulator)
+ language: script
+ args: ["--module-path", "source/modules/cms_vehicle_simulator", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/cms_vehicle_simulator
+ verbose: true
+ require_serial: true
+ - id: cms-fleetwise-connector
+ name: (FleetWise Connector)
+ language: script
+ args: ["--module-path", "source/modules/cms_fleetwise_connector", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/cms_fleetwise_connector
+ verbose: true
+ require_serial: true
+ - id: auth-setup
+ name: (Auth Setup)
+ language: script
+ args: ["--module-path", "source/modules/auth_setup", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/auth_setup
+ verbose: true
+ require_serial: true
+ - id: vpc
+ name: (VPC)
+ language: script
+ args: ["--module-path", "source/modules/vpc", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^source/modules/vpc
+ verbose: true
+ require_serial: true
+ - id: nightswatch
+ name: (NightsWatch)
language: script
- args: ["--module", "vehicle_simulator", "--files-list"]
- entry: ./deployment/run_module_hooks.py
- files: ^templates/modules/cms_vehicle_simulator_on_aws
+ args: ["--module-path", ".nightswatch", "--files-list"]
+ entry: ./deployment/script_run_module_hooks.py
+ files: ^.nightswatch
verbose: true
require_serial: true
diff --git a/.python-version b/.python-version
index 54c5196a..c8cfe395 100644
--- a/.python-version
+++ b/.python-version
@@ -1 +1 @@
-3.10.9
+3.10
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a73b6797..5c2b3dd8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,11 +5,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.1.0] - 2024-04-11
+
+### Added
+
+#### Common
+
+- Added all applicable ACDP and CMS on AWS module resources to VPC.
+- Created VPC module to provide a reference VPC implementation for ACDP and CMS on AWS Modules.
+- Added one-click deployment support via CloudFormation templates.
+- Created Make targets for build, upload, and deploy process.
+
+#### ACDP
+
+- Replaced AWS Proton with custom build orchestration via Amazon CodeBuild from Backstage.
+- Created ACDP plugins for Backstage to assist with CI/CD operations of CMS on AWS modules and external solutions.
+
+#### CMS
+
+- Created Auth Setup module which adds support for choosing between Cognito or a compatible OAuth 2.0 compliant IdP.
+- Created CMS Config module to define common configurations within the solution.
+- Added TechDocs support to CMS Modules.
+
+### Fixed
+
+- Updates to Backstage to resolve various issues in plugins.
+
## [1.0.4] - 2024-02-28
### Fixed
-- Upgrade backstage to 1.23.3 to mitigate vulnerability
+- Upgrade backstage to 1.23.3 to mitigate vulnerability.
- Fix a bug that could occur if the s3 version of the backstage source was prefixed with a special character.
@@ -17,30 +43,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
-- Added resolution for the ECDSA package to mitigate vulnerability
-- Added resolution for the cyrptography package to mitigate vulnerability
-- Added resolution for node-ip package to mitigate vulnerability
+- Added resolution for the ECDSA package to mitigate vulnerability.
+- Added resolution for the cyrptography package to mitigate vulnerability.
+- Added resolution for node-ip package to mitigate vulnerability.
## [1.0.2] - 2024-01-10
### Fixed
-- Updated Grafana workspace in EV Battery Health module to include
-plugin management and install Amazon Athena plugin
-- Added resolution for octokit package to mitigate vulnerability
-- Added resolution for follow-redirects package to mitigate vulnerability
-- Added resolution for swagger-ui-react package to address Backstage build failure
-- Removed `yarn tsc:full` from backstage image build
-- Add ignore pattern for Axios in vehicle simulator to ensure correct version usage
+- Updated Grafana workspace in EV Battery Health module to include.
+plugin management and install Amazon Athena plugin.
+- Added resolution for octokit package to mitigate vulnerability.
+- Added resolution for follow-redirects package to mitigate vulnerability.
+- Added resolution for swagger-ui-react package to address Backstage build failure.
+- Removed `yarn tsc:full` from backstage image build.
+- Add ignore pattern for Axios in vehicle simulator to ensure correct version usage.
## [1.0.1] - 2023-11-15
### Fixed
-- Updated various README URLs to the correct values
-- Resolved an issue where the Aurora PostgresSQL cluster's version defaulted to 11 instead of 13 in some regions
-- Pinned Node and Python versions in Proton manifest.yml for every module
+- Updated various README URLs to the correct values.
+- Resolved an issue where the Aurora PostgresSQL cluster's version defaulted to 11 instead of 13 in some regions.
+- Pinned Node and Python versions in Proton manifest.yml for every module.
## [1.0.0] - 2023-09-05
@@ -58,8 +84,8 @@ plugin management and install Amazon Athena plugin
#### Automotive Cloud Developer Portal
-- CMS Backstage Deployment
-- CMS Module Deployment Templates for Backstage
-- Proton Deployment Support
-- S3 Backend Support for Backstage Assets
-- Authentication and User flow implementation with Cognito
+- Add CMS Backstage Deployment.
+- Add CMS Module Deployment Templates for Backstage.
+- Add Proton Deployment Support.
+- Add S3 Backend Support for Backstage Assets.
+- Authentication and User flow implementation with Cognito.
diff --git a/Makefile b/Makefile
index 2af60205..61592e30 100644
--- a/Makefile
+++ b/Makefile
@@ -1,253 +1,243 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
--include .env
-
.DEFAULT_GOAL := help
+SHELL := /bin/bash
-# ========================================================
-# VARIABLES
-# ========================================================
-PYTHON_VERSION ?= 3.10.9
-AWS_ACCOUNT_ID=$(shell aws sts get-caller-identity --query "Account" --output text)
-PIPENV_VENV_IN_PROJECT = 1
-STAGE ?= dev
-AWS_REGION ?= $(shell aws configure get region --output text)
-CDK_DEPLOY_REGION = ${AWS_REGION}
-ROUTE53_BASE_DOMAIN ?= ${ROUTE53_ZONE_NAME}
-BACKSTAGE_WEB_PORT ?= 443
-BACKSTAGE_WEB_SCHEME ?= https
-VPC_CIDR_RANGE ?= 10.0.0.0/16
-BACKSTAGE_LOG_LEVEL ?= info
-CMS_SOLUTION_VERSION ?= v0.0.0
-CMS_RESOURCE_BUCKET ?= ${AWS_ACCOUNT_ID}-cms-resources-${AWS_REGION}
-CMS_RESOURCE_BUCKET_REGION ?= ${AWS_REGION}
-BACKSTAGE_TEMPLATE_S3_KEY_PREFIX ?= ${CMS_SOLUTION_VERSION}/backstage/templates
-BACKSTAGE_TEMPLATE_S3_UPDATE_REFRESH_MINS ?= 30
-BACKSTAGE_NAME ?= DEFAULT_NAME
-BACKSTAGE_ORG ?= DEFAULT_ORG
-
-# Call export after all variables are set.
-# This alllows Make variables to be used and environment variables in sub-shells created by Make target commands
-export
-
-# ==================================================================================
-# PRINT COLORS
-# To use, simply add ${} to get the colored text.
-# To disable color, add ${NC} at the point you'd like it to stop.
-# printf is recommended over echo if wanting color because of more multi-platform support.
-# ==================================================================================
-LIGHT_GREEN = \033[1;32m
-GREEN = \033[0;32m
-LIGHT_PURPLE = \033[1;35m
-NC = \033[00m
-
-.PHONY: install
-install: pipenv-install pipenv-clean node-package-install ## Installs the resources and dependencies required to build the solution.
- @printf "${LIGHT_PURPLE}Install finished.${NC}\n"
-
-.PHONY: node-package-install
-node-package-install: ## Using npm, installs yarn, the aws-cdk-lib, and node dependencies for all modules.
- @printf "${LIGHT_PURPLE}Checking for yarn installation and installing if not found.${NC}\n"
- npm install -g yarn
- @printf "${LIGHT_PURPLE}Checking for cdk installation and installing if not found.${NC}\n"
- npm install -g aws-cdk
- @printf "${LIGHT_PURPLE}Installing node dependencies using yarn.${NC}\n"
- find . -name "package.json" -not -path "*node_modules*" -not -path "*cdk-solution-helper*" -not -path "*cdk.out*" -path "*backstage*" -not -path "*examples*" -execdir bash -c "echo 'Installing from yarn '; pwd; yarn install " {} \;
- @printf "${LIGHT_PURPLE}Installing node dependencies using npm.${NC}\n"
- find . -name "package.json" -not -path "*node_modules*" -not -path "*cdk-solution-helper*" -not -path "*cdk.out*" -not -path "*backstage*" -execdir bash -c "echo 'Installing from npm '; pwd; npm install;" {} \;
-
-.PHONY: pipenv-install
-pipenv-install: ## Using pipenv, installs pip dependencies for all modules.
- @printf "${LIGHT_PURPLE}Installing pip dependencies.${NC}\n"
- find . -name "Pipfile" -not -path "*cdk.out*" -exec bash -c "echo; echo 'Installing from ' {}; PIPENV_IGNORE_VIRTUALENVS=1 PIPENV_PIPFILE={} PIPENV_VENV_IN_PROJECT=1 pipenv install --dev --python ${PYTHON_VERSION}" {} \;
-
-.PHONY: gen-python-requirements
-gen-python-requirements: ## Generates requirements.txt files from the pipfiles throughout the solution.
- @printf "${LIGHT_PURPLE}Generating requirements.txt from pipfiles.${NC}\n"
- find . -name "Pipfile" -not -path "*cdk.out*" -execdir bash -c "echo; PIPENV_IGNORE_VIRTUALENVS=1 PIPENV_PIPFILE={} PIPENV_VENV_IN_PROJECT=1 pipenv requirements 1> requirements.txt; echo" {} \;
+include makefiles/common_config.mk
+include makefiles/global_targets.mk
## ========================================================
-## PIPENV VIRTUAL ENVIRONMENT MANAGEMENT
+## INCLUDE MODULE'S MAKEFILE TARGETS
## ========================================================
-.PHONY: pipenv-lock
-pipenv-lock: ## Generates Pipfile.lock for all modules (pipenv lock).
- @printf "${LIGHT_PURPLE}Generating Pipfile.lock from Pipfiles.${NC}\n"
- find . -name "Pipfile" -not -path "*cdk.out*" -exec bash -c "echo; echo 'Installing from ' {}; PIPENV_IGNORE_VIRTUALENVS=1 PIPENV_PIPFILE={} PIPENV_VENV_IN_PROJECT=1 pipenv lock --dev --python ${PYTHON_VERSION}" {} \;
-
-.PHONY: pipenv-sync
-pipenv-sync: ## Installs all packages specified in Pipfile.lock for all modules (pipenv sync).
- @printf "${LIGHT_PURPLE}Syncing virtual environments with Pipfile.lock.${NC}\n"
- find . -name "Pipfile" -not -path "*cdk.out*" -exec bash -c "echo; echo 'Installing from ' {}; PIPENV_IGNORE_VIRTUALENVS=1 PIPENV_PIPFILE={} PIPENV_VENV_IN_PROJECT=1 pipenv sync --dev --python ${PYTHON_VERSION}" {} \;
-
-.PHONY: pipenv-update
-pipenv-update: ## Runs lock then sync. (pipenv update).
- @printf "${LIGHT_PURPLE}Beginning pipenv update (lock and sync).${NC}\n"
- find . -name "Pipfile" -not -path "*cdk.out*" -exec bash -c "echo; echo 'Updating from ' {}; PIPENV_IGNORE_VIRTUALENVS=1 PIPENV_PIPFILE={} PIPENV_VENV_IN_PROJECT=1 pipenv update --dev --python ${PYTHON_VERSION}" {} \;
-
-.PHONY: pipenv-clean
-pipenv-clean: ## Uninstalls all packages not specified in Pipfile.lock (pipenv clean).
- @printf "${LIGHT_PURPLE}Cleaning virtual environment of packages not in Pipfile.lock.${NC}\n"
- find . -name "Pipfile" -not -path "*cdk.out*" -exec bash -c "echo; echo 'Cleaning from ' {}; PIPENV_IGNORE_VIRTUALENVS=1 PIPENV_PIPFILE={} PIPENV_VENV_IN_PROJECT=1 pipenv clean --dry-run --python ${PYTHON_VERSION}" {} \;
+module_name-target: ## Call a module make target. Run "make module_name-help" for target lists. Run "ls source/modules" for module list.
+
+MODULES := source/lib $(shell find ${SOLUTION_PATH}/source/modules -type d -maxdepth 1 -mindepth 1 -not -name __pycache__)
+GLOBAL_TARGETS := $(shell grep -E '^[a-zA-Z0-9-]+:' ${SOLUTION_PATH}/makefiles/global_targets.mk | awk -F: '/^[^.]/ {print $$1;}')
+COMMON_TARGETS := $(shell grep -E '^[a-zA-Z0-9-]+:' ${SOLUTION_PATH}/makefiles/module_targets.mk | awk -F: '/^[^.]/ {print $$1;}')
+define make-module-target
+$(lastword $(subst /, ,$2))-$1:
+ @$(MAKE) -C $2 -f Makefile $1
+endef
+$(foreach module,$(MODULES),$(foreach element,$(shell grep -E '^[a-zA-Z0-9-]+:' $(module)/Makefile | awk -F: '/^[^.]/ {print $$1;}'),$(eval $(call make-module-target,$(element),$(module)))))
+$(foreach module,$(MODULES),$(foreach target,$(GLOBAL_TARGETS),$(eval $(call make-module-target,$(target),$(module)))))
+$(foreach module,$(MODULES),$(foreach target,$(COMMON_TARGETS),$(eval $(call make-module-target,$(target),$(module)))))
## ========================================================
-## SYNTH AND DEPLOY
+## INVOKE MAKE TARGET FROM EACH MODULES' MAKEFILE
## ========================================================
+SubMakefiles = source/lib/ $(shell find source \( -name lib -o -name deployment -o -name cdk.out -o -name .venv -o -name node_modules -o -name backstage \) -prune -false -o -name Makefile)
+SubMakeDirs = $(filter-out ${SOLUTION_PATH},$(dir $(SubMakefiles)))
+Prereqs = source/modules/vpc/ source/modules/auth_setup/ source/modules/cms_config/ source/modules/cms_auth/ source/modules/cms_connect_store/ source/modules/cms_alerts/ source/modules/cms_api/
+DeployableDirs = $(filter-out source/lib/ source/modules/cms_sample_on_aws ${Prereqs},${SubMakeDirs})
+
+define run-module-target
+ run_make_with_logging() { \
+ output=$$(make -C "$$1" $1 2>&1); \
+ module_target_exit_code=$$?; \
+ if [[ $$module_target_exit_code -ne 0 ]]; then \
+ printf "%bFinished %sMakefile %s failed.\n%s\n%b\n" "${RED}" "$$1" "$1" "$$output" "${NC}"; \
+ else \
+ printf "%bFinished %sMakefile %s passed.%b\n" "${GREEN}" "$$1" "$1" "${NC}"; \
+ fi; \
+ return $$module_target_exit_code; \
+ }; \
+ did_make_target_fail=0; \
+ process_pids=(); \
+ for dir in $2; do \
+ printf "%bStarting %sMakefile %s.%b\n" "${MAGENTA}" "$$dir" "$1" "${NC}"; \
+ (run_make_with_logging "$$dir") & process_pids+=($$!); \
+ done; \
+ for pid in $${process_pids[@]}; do wait "$${pid}" || did_make_target_fail=1; done; \
+ exit $$did_make_target_fail;
+endef
-.PHONY: synth
-synth: ## Runs cdk synth for Backstage and CMS core.
- @printf "${LIGHT_PURPLE}Synthesizing Backstage and CMS core.${NC}\n"
- cdk synth \
- --context "user-email"="${USER_EMAIL}" \
- --context "route53-zone-name"="${ROUTE53_ZONE_NAME}" \
- --context "route53-base-domain"="${ROUTE53_BASE_DOMAIN}" \
- --context "web-port"="${BACKSTAGE_WEB_PORT}" \
- --context "web-scheme"="${BACKSTAGE_WEB_SCHEME}" \
- --context "vpc-cidr-range"="${VPC_CIDR_RANGE}" \
- --context "backstage-name"="${BACKSTAGE_NAME}" \
- --context "backstage-org"="${BACKSTAGE_ORG}" \
- --context "backstage-log-level"="${BACKSTAGE_LOG_LEVEL}" \
- --context "cms-resource-bucket"="${CMS_RESOURCE_BUCKET}" \
- --context "cms-resource-bucket-region"="${CMS_RESOURCE_BUCKET_REGION}" \
- --context "cms-resource-bucket-backstage-template-key-prefix"="${BACKSTAGE_TEMPLATE_S3_KEY_PREFIX}" \
- --context "cms-resource-bucket-backstage-refresh-frequency-mins"="${BACKSTAGE_TEMPLATE_S3_UPDATE_REFRESH_MINS}" \
- --context "nag-enforce"=True \
- --quiet
-
-
-.PHONY: synth-staging
-synth-staging: ## Runs cdk synth for Backstage and CMS core, and ouputs to ./deployment/staging.
- @printf "${LIGHT_PURPLE}Synthesizing Backstage and CMS core for staging (./deployment/staging).${NC}\n"
- cdk synth \
- --context "user-email"="${USER_EMAIL}" \
- --context "route53-zone-name"="${ROUTE53_ZONE_NAME}" \
- --context "route53-base-domain"="${ROUTE53_BASE_DOMAIN}" \
- --context "web-port"="${BACKSTAGE_WEB_PORT}" \
- --context "web-scheme"="${BACKSTAGE_WEB_SCHEME}" \
- --context "vpc-cidr-range"="${VPC_CIDR_RANGE}" \
- --context "backstage-name"="${BACKSTAGE_NAME}" \
- --context "backstage-org"="${BACKSTAGE_ORG}" \
- --context "backstage-log-level"="${BACKSTAGE_LOG_LEVEL}" \
- --context "cms-resource-bucket"="${CMS_RESOURCE_BUCKET}" \
- --context "cms-resource-bucket-region"="${CMS_RESOURCE_BUCKET_REGION}" \
- --context "cms-resource-bucket-backstage-template-key-prefix"="${BACKSTAGE_TEMPLATE_S3_KEY_PREFIX}" \
- --context "cms-resource-bucket-backstage-refresh-frequency-mins"="${BACKSTAGE_TEMPLATE_S3_UPDATE_REFRESH_MINS}" \
- --context "nag-enforce"=True \
- --output="./deployment/staging" \
- --quiet
-
-.PHONY: cdk-context
-cdk-context: check-cdk-env ## Displays current cdk context.
- @printf "${LIGHT_PURPLE}Verifying CDK Context.${NC}\n"
- cdk context \
- --context "user-email"="${USER_EMAIL}" \
- --context "route53-zone-name"="${ROUTE53_ZONE_NAME}" \
- --context "route53-base-domain"="${ROUTE53_BASE_DOMAIN}" \
- --context "web-port"="${BACKSTAGE_WEB_PORT}" \
- --context "web-scheme"="${BACKSTAGE_WEB_SCHEME}" \
- --context "vpc-cidr-range"="${VPC_CIDR_RANGE}" \
- --context "backstage-name"="${BACKSTAGE_NAME}" \
- --context "backstage-org"="${BACKSTAGE_ORG}" \
- --context "backstage-log-level"="${BACKSTAGE_LOG_LEVEL}" \
- --context "cms-resource-bucket"="${CMS_RESOURCE_BUCKET}" \
- --context "cms-resource-bucket-region"="${CMS_RESOURCE_BUCKET_REGION}" \
- --context "cms-resource-bucket-backstage-template-key-prefix"="${BACKSTAGE_TEMPLATE_S3_KEY_PREFIX}" \
- --context "cms-resource-bucket-backstage-refresh-frequency-mins"="${BACKSTAGE_TEMPLATE_S3_UPDATE_REFRESH_MINS}"
+.PHONY: install
+install: root-install ## Call root and all modules' "make install".
+ @$(call run-module-target,install,${SubMakeDirs})
+ @printf "%bFinished install.%b\n" "${GREEN}" "${NC}"
+.PHONY: build
+build: ## Call all modules' "make build".
+ @printf "%bStarting build.%b\n" "${MAGENTA}" "${NC}"
+ @$(call run-module-target,build,${SubMakeDirs})
+ @printf "%bFinished build.%b\n" "${GREEN}" "${NC}"
.PHONY: deploy
-deploy: check-cdk-env clean ## Runs make clean, then builds and deploys Backstage and CMS core.
- @printf "${LIGHT_PURPLE}Deploying Backstage and CMS core.${NC}\n"
- cdk deploy \
- --context "user-email"="${USER_EMAIL}" \
- --context "route53-zone-name"="${ROUTE53_ZONE_NAME}" \
- --context "route53-base-domain"="${ROUTE53_BASE_DOMAIN}" \
- --context "web-port"="${BACKSTAGE_WEB_PORT}" \
- --context "web-scheme"="${BACKSTAGE_WEB_SCHEME}" \
- --context "vpc-cidr-range"="${VPC_CIDR_RANGE}" \
- --context "backstage-name"="${BACKSTAGE_NAME}" \
- --context "backstage-org"="${BACKSTAGE_ORG}" \
- --context "backstage-log-level"="${BACKSTAGE_LOG_LEVEL}" \
- --context "cms-resource-bucket"="${CMS_RESOURCE_BUCKET}" \
- --context "cms-resource-bucket-region"="${CMS_RESOURCE_BUCKET_REGION}" \
- --context "cms-resource-bucket-backstage-template-key-prefix"="${BACKSTAGE_TEMPLATE_S3_KEY_PREFIX}" \
- --context "cms-resource-bucket-backstage-refresh-frequency-mins"="${BACKSTAGE_TEMPLATE_S3_UPDATE_REFRESH_MINS}"
-
-.PHONY: bootstrap
-bootstrap: check-cdk-env ## Bootstraps Backstage and CMS core.
- @printf "${LIGHT_PURPLE}Bootstrapping Backstage and CMS core.${NC}\n"
- cdk bootstrap \
- --context "user-email"="${USER_EMAIL}" \
- --context "route53-zone-name"=${ROUTE53_ZONE_NAME} \
- --context "route53-base-domain"=${ROUTE53_BASE_DOMAIN} \
- --context "web-port"=${BACKSTAGE_WEB_PORT} \
- --context "web-scheme"=${BACKSTAGE_WEB_SCHEME} \
- --context "vpc-cidr-range"=${VPC_CIDR_RANGE} \
- --context "backstage-name"="${BACKSTAGE_NAME}" \
- --context "backstage-org"="${BACKSTAGE_ORG}" \
- --context "backstage-log-level"="${BACKSTAGE_LOG_LEVEL}" \
- --context "cms-resource-bucket"="${CMS_RESOURCE_BUCKET}" \
- --context "cms-resource-bucket-region"="${CMS_RESOURCE_BUCKET_REGION}" \
- --context "cms-resource-bucket-backstage-template-key-prefix"="${BACKSTAGE_TEMPLATE_S3_KEY_PREFIX}" \
- --context "cms-resource-bucket-backstage-refresh-frequency-mins"="${BACKSTAGE_TEMPLATE_S3_UPDATE_REFRESH_MINS}"
-
-.PHONY: upload-s3-deployment-assets
-upload-s3-deployment-assets: clean ## Runs make clean, then uploads required deployment assets to S3 for deploying CMS modules via Backstage and Proton.
- @printf "${LIGHT_PURPLE}Beginning S3 setup.${NC}\n"
- @printf "${LIGHT_PURPLE}Creating and uploading proton service templates (./deployment/create-proton-service-templates.sh).${NC}\n"
- ./deployment/create-proton-service-templates.sh
- @printf "${LIGHT_PURPLE}Copying module source code and template.yaml files to S3. (./deployment/copy-backstage-templates-to-s3.sh).${NC}\n"
- ./deployment/copy-backstage-templates-to-s3.sh
- @printf "${LIGHT_PURPLE}Finished setting up S3.${NC}\n"
-
-.PHONY: get-deployment-uuid
-get-deployment-uuid: ## Retrieves the deployment-uuid value from the ssm parameter in your AWS account
- @printf "${LIGHT_PURPLE}Retrieving Deplyoment UUID.${NC}\n"
- aws ssm get-parameter --name=/${STAGE}/cms/common/config/deployment-uuid --query Parameter.Value --output text
+deploy: deploy-variables ## Call all modules' "make deploy". Order enforced.
+ @printf "%bStarting deploy.%b\n" "${MAGENTA}" "${NC}"
+ @for dir in $(Prereqs); do \
+ printf "%bDeploying %s.%b\n" "${MAGENTA}" "$$dir" "${NC}"; \
+ $(MAKE) -C $$dir deploy || exit $$?; \
+ done
+ @$(call run-module-target,deploy,${DeployableDirs})
+ @printf "%bFinished deploy.%b\n" "${GREEN}" "${NC}"
+ @printf "%bView status:%b %bhttps://%s.console.aws.amazon.com/cloudformation/home?region=%s%b\n" "${YELLOW}" "${NC}" "${CYAN}" "${AWS_REGION}" "${AWS_REGION}" "${NC}"
+
+.PHONY: destroy
+destroy: ## Call all modules' "make destroy". Order enforced.
+ @printf "%bStarting destroy.%b\n" "${MAGENTA}" "${NC}"
+ @$(call run-module-target,destroy,${DeployableDirs})
+ @reversed=$$(printf "%s\n" ${Prereqs} | tail -r | xargs echo); \
+ for dir in $${reversed}; do \
+ printf "%bDestroying %s.%b\n" "${MAGENTA}" "$$dir" "${NC}"; \
+ $(MAKE) -C $$dir destroy || exit $$?; \
+ done
+ @printf "%bFinished destroy.%b\n" "${GREEN}" "${NC}"
+ @printf "%bView status:%b %bhttps://%s.console.aws.amazon.com/cloudformation/home?region=%s%b\n" "${YELLOW}" "${NC}" "${CYAN}" "${AWS_REGION}" "${AWS_REGION}" "${NC}"
+
+.PHONY: upload
+upload: create-upload-bucket upload-backstage-assets-zip ## Call root and all modules' "make upload" and upload backstage assets zip.
+ @$(call run-module-target,upload,${SubMakeDirs})
+ @printf "%bFinished upload.%b\n" "${MAGENTA}" "${NC}"
+ @printf "%bView resources:%b %bhttps://s3.console.aws.amazon.com/s3/buckets/%s-%s?region=%s%b\n" "${YELLOW}" "${NC}" "${CYAN}" "${S3_ASSET_BUCKET_BASE_NAME}" "${AWS_REGION}" "${AWS_REGION}" "${NC}"
+
+.PHONY: upload-backstage-assets-zip
+upload-backstage-assets-zip:
+ @aws s3api put-object \
+ --bucket "${REGIONAL_ASSET_BUCKET_NAME}" \
+ --key "${SOLUTION_NAME}/${SOLUTION_VERSION}/backstage.zip" \
+ --body "${SOLUTION_PATH}/deployment/regional-s3-assets/backstage.zip" \
+ --expected-bucket-owner "${AWS_ACCOUNT_ID}" > /dev/null
+ @printf "%bFinished uploading zipped backstage assets \n%b" "${GREEN}" "${NC}"
+
+.PHONY: verify-module
+verify-module: ## Run all verifications for CMS. CAUTION: Takes a long time.
+ @$(call run-module-target,verify-module,${SubMakeDirs})
+ @printf "%bFinished verify-module.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: cfn-nag
+cfn-nag: ## Run cfn-nag for the entire solution.
+ @$(call run-module-target,cfn-nag,${SubMakeDirs})
+ @printf "%bFinished cfn-nag.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: unit-tests
+unit-tests: ## Run unit-tests for the entire solution.
+ @$(call run-module-target,unit-tests,${SubMakeDirs})
+ @printf "%bFinished unit tests.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: test
+test: ## Run cfn-nag and unit-tests for the entire solution.
+ @$(call run-module-target,test,${SubMakeDirs})
+ @printf "%bFinished test.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: update-snapshots
+update-snapshots: ## Run update-snapshots for the entire solution.
+ @$(call run-module-target,update-snapshots,${SubMakeDirs})
+ @printf "%bFinished update-snapshots.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: version
+version: root-version ## Display solution name and current version and each module's version
+ @process_pids=(); \
+ for dir in $(SubMakeDirs); do $(MAKE) -C $$dir version & process_pids+=($$!); done; \
+ for pid in $${process_pids[@]}; do wait "$${pid}"; done;
+
+## ========================================================
+## INSTALL
+## ========================================================
+.PHONY: root-install
+root-install: ## Using pipenv, installs pip dependencies for root.
+ @printf "%bInstalling pip dependencies.%b\n" "${MAGENTA}" "${NC}"
+ pipenv install --dev --python ${PYTHON_VERSION}
+ pipenv clean --python ${PYTHON_VERSION}
## ========================================================
-## UTILITY COMMANDS
+## BUILD
## ========================================================
-.PHONY: clean
-clean: ## Cleans up existing build files, not including venvs or dependencies.
- @printf "${LIGHT_PURPLE}Running clean scripts.${NC}\n"
- ./deployment/clean-for-deploy.sh
-
-.PHONY: check-cdk-env
-check-cdk-env: ## Checks the cdk environment for the required environment variables and dependencies.
-ifneq (v18.17.1, $(shell node --version))
- $(error Node version 18.17.1 is required, as specified in .nvmrc. Please install by running `nvm install`)
-endif
-ifneq (9.6.7, $(shell npm --version))
- $(error Npm version 3.10.9 is required, as specified by the node version in .nvmrc. Please check your node installation.`)
-endif
-ifneq (Python 3.10.9, $(shell python --version))
- $(error Python version 3.10.9 is required, as specified in .python-version. Please install by running `pyenv install -s`)
-endif
-ifneq (, $(wildcard ./cdk.context.json))
- $(error 'cdk.context.json' cannot exist, please delete and try again)
-endif
-ifndef USER_EMAIL
- $(error USER_EMAIL is undefined. Set the variable using `export USER_EMAIL=...`, or use a .env file)
-endif
-ifndef ROUTE53_ZONE_NAME
- $(error ROUTE53_ZONE_NAME is undefined. Set the variable using `export USER_EMAIL=...`, or use a .env file)
-endif
-ifndef ROUTE53_BASE_DOMAIN
- $(error ROUTE53_BASE_DOMAIN is undefined. Set the variable using `export USER_EMAIL=...`, or use a .env file)
-endif
- @printf "${GREEN}All required environment variables found.${NC}\n"
+.PHONY: asset-copy
+asset-copy: ## Copy modules' build artifacts to root level folders
+ @printf "%bCopying global assets to ${SOLUTION_PATH}/deployment%b\n" "${MAGENTA}" "${NC}"
+ @rm -rf ${SOLUTION_PATH}/deployment/global-s3-assets && mkdir ${SOLUTION_PATH}/deployment/global-s3-assets
+ @find source \( -name cdk.out -o -name .venv -o -name node_modules -o -name backstage -o -name build \) -prune -false -o -name "global-s3-assets" -exec bash -c "cp -r {}/* ${SOLUTION_PATH}/deployment/global-s3-assets" \;
+ @printf "%bCopying regional assets to ${SOLUTION_PATH}/deployment%b\n" "${MAGENTA}" "${NC}"
+ @rm -rf ${SOLUTION_PATH}/deployment/regional-s3-assets && mkdir ${SOLUTION_PATH}/deployment/regional-s3-assets
+ @find source \( -name cdk.out -o -name .venv -o -name node_modules -o -name backstage -o -name build \) -prune -false -o -name "regional-s3-assets" -exec bash -c "cp -r {}/* ${SOLUTION_PATH}/deployment/regional-s3-assets" \;
+ @printf "%bFinished asset collation.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: zip-backstage-assets
+zip-backstage-assets: ## Zip backstage assets in the regional assets directory
+ @cd ${SOLUTION_PATH}/deployment/regional-s3-assets/backstage && zip -r ${SOLUTION_PATH}/deployment/regional-s3-assets/backstage.zip . > /dev/null
+ @printf "%bFinished zipping backstage assets \n%b" "${GREEN}" "${NC}"
+
+.PHONY: build-open-source
+build-open-source: ## Build open source distribution
+ ${SOLUTION_PATH}/deployment/build-open-source-dist.sh --solution-name ${SOLUTION_NAME}
+
+.PHONY: build-all
+build-all: build asset-copy zip-backstage-assets ## Builds all modules and copies assets to top level deployment folder.
## ========================================================
-## HELP COMMANDS
+## TESTING
+## ========================================================
+.PHONY: pre-commit-all
+pre-commit-all: ## Run pre-commit for the entire solution for all files.
+ @printf "%bRunning all pre-commits.%b\n" "${MAGENTA}" "${NC}"
+ -pipenv run pre-commit run --all-files
+
+## ========================================================
+## UTILITY
+## ========================================================
+.PHONY: clean-build-artifacts
+clean-build-artifacts: ## Cleans up build files, not including venvs, dependencies, or release build artifacts.
+ @printf "%bRunning clean script.%b\n" "${MAGENTA}" "${NC}"
+ ${SOLUTION_PATH}/deployment/run-clean-build-artifacts.sh
+ @printf "%bFinished clean script.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: clean-build-artifacts-release
+clean-build-artifacts-release: ## Cleans up build files, including release build artifacts.
+ @printf "%bRunning clean script.%b\n" "${MAGENTA}" "${NC}"
+ ${SOLUTION_PATH}/deployment/run-clean-build-artifacts.sh --release-build
+ @printf "%bFinished clean script.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: clean-build-artifacts-dependencies
+clean-build-artifacts-dependencies: ## Cleans up build files, including venvs and dependencies.
+ @LOCK_FILES_OPTION="--lock-files"; \
+ if [ "$${PIPELINE_TYPE}" = "dtas" ]; then \
+ LOCK_FILES_OPTION=""; \
+ fi; \
+ printf "%bRunning clean scripts.%b\n" "${MAGENTA}" "${NC}"; \
+ ${SOLUTION_PATH}/deployment/run-clean-build-artifacts.sh --dependencies $$LOCK_FILES_OPTION
+ @printf "%bFinished clean script.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: clean-build-artifacts-all
+clean-build-artifacts-all: ## Cleans up existing build files, including venvs, dependencies, and release build artifacts.
+ @printf "%bRunning clean script.%b\n" "${MAGENTA}" "${NC}"
+ ${SOLUTION_PATH}/deployment/run-clean-build-artifacts.sh --all
+ @printf "%bFinished clean script.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: deploy-variables
+deploy-variables: ## Get variable values to deploy with.
+ @[[ -f .cmsrc ]] || printf "%bInstead of using this target, you can run the following command.\n%b" "${MAGENTA}" "${NC}"
+ @[[ -f .cmsrc ]] || printf "%bcat > .cmsrc < .cmsrc
+
+.PHONY: generate-python-requirements-files
+generate-python-requirements-files: ## Generates requirements.txt files from the pipfiles throughout the solution.
+ @printf "%bGenerating requirements.txt from pipfiles.%b\n" "${MAGENTA}" "${NC}"\
+ find ${SOLUTION_PATH} \( -name .venv -o -name node_modules -o -name "cdk.out" \) -prune -false -o -name "Pipfile" -execdir bash -c "echo; PIPENV_PIPFILE={} pipenv requirements 1> requirements.txt;" \;
+
+## ========================================================
+## HELPERS
## ========================================================
.PHONY: help
-help: ## Displays usage information about the Makefile in a readable format.
- @grep -E '^[a-zA-Z0-9 -]+:.*##|^##.*' Makefile | while read -r l; \
- do ( [[ "$$l" =~ ^"##" ]] && printf "${LIGHT_PURPLE}%s${NC}\n" "$$(echo $$l | cut -f 2- -d' ')") \
- || ( printf "${LIGHT_GREEN}%-30s${NC}%s\n" "$$(echo $$l | cut -f 1 -d':')" "$$(echo $$l | cut -f 3- -d'#')"); \
+help: ## Displays this help message. For a module's help, run "make -help".
+ @grep -E '^[a-zA-Z0-9 -_]+:.*##|^##.*' ${SOLUTION_PATH}/Makefile | while read -r l; \
+ do ( [[ "$$l" =~ ^"##" ]] && printf "%b%s%b\n" "${MAGENTA}" "$$(echo $$l | cut -f 2- -d' ')" "${NC}") \
+ || ( printf "%b%-35s%s%b\n" "${GREEN}" "$$(echo $$l | cut -f 1 -d':')" "$$(echo $$l | cut -f 3- -d'#')" "${NC}"); \
done;
-.PHONY: list-rules
-list-rules: ## Displays an alphabetical list of the makefile rules with their descriptions.
- @grep -E '^[a-zA-Z0-9 -]+:.*##' Makefile | sort | while read -r l; do printf "${LIGHT_GREEN}%-30s${NC}%s\n" "$$(echo $$l | cut -f 1 -d':')" "$$(echo $$l | cut -f 3- -d'#')"; done
+.PHONY: encourage
+encourage: ## Sometimes we all need a little encouragement!
+ @printf "%bYou can do this. Believe in yourself. :)%b\n" "${GREEN}" "${NC}"
+
+.PHONY: root-version
+root-version: ## Display solution name and current version
+ @printf "%b%35.35s%b version:%b%s%b\n" "${MAGENTA}" "${SOLUTION_NAME}" "${NC}" "${GREEN}" "${SOLUTION_VERSION}" "${NC}"
diff --git a/NOTICE.txt b/NOTICE.txt
index 38be2543..8c541986 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -26,8 +26,6 @@ This software includes third party software subject to the following copyrights:
@aws-sdk/signature-v4 under the Apache License 2.0
@aws/aws-codeservices-backend-plugin-for-backstage under the Apache License 2.0
@aws/aws-codeservices-plugin-for-backstage under the Apache License 2.0
-@aws/aws-proton-backend-plugin-for-backstage under the Apache License 2.0
-@aws/aws-proton-plugin-for-backstage under the Apache License 2.0
@backstage/app-defaults under the Apache License 2.0
@backstage/backend-common under the Apache License 2.0
@backstage/backend-tasks under the Apache License 2.0
@@ -115,7 +113,6 @@ rsa under the Apache License 2.0
s3transfer under the Apache License 2.0
types-pyasn1 under the Apache License 2.0
types-python-dateutil under the Apache License 2.0
-types-python-jose under the Apache License 2.0
types-PyYAML under the Apache License 2.0
types-setuptools under the Apache License 2.0
typescript under the Apache License 2.0
@@ -159,7 +156,6 @@ cross-env under the Massachusetts Institute of Technology (MIT) license
cypress under the Massachusetts Institute of Technology (MIT) license
dataclass-type-validator under the Massachusetts Institute of Technology (MIT) license
dparse under the Massachusetts Institute of Technology (MIT) license
-ecdsa under the Massachusetts Institute of Technology (MIT) license
eslint-plugin-cypress under the Massachusetts Institute of Technology (MIT) license
exceptiongroup under the Massachusetts Institute of Technology (MIT) license
express-promise-router under the Massachusetts Institute of Technology (MIT) license
@@ -199,7 +195,6 @@ pyrsistent under the Massachusetts Institute of Technology (MIT) license
pytest under the Massachusetts Institute of Technology (MIT) license
pytest-cov under the Massachusetts Institute of Technology (MIT) license
pytest-mock under the Massachusetts Institute of Technology (MIT) license
-python-jose under the Massachusetts Institute of Technology (MIT) license
react under the Massachusetts Institute of Technology (MIT) license
react-bootstrap under the Massachusetts Institute of Technology (MIT) license
react-dom under the Massachusetts Institute of Technology (MIT) license
@@ -275,7 +270,3 @@ pathspec under the Mozilla Public License 2.0 (MPL 2.0)
typing_extensions under the PSF license
uuid under Unknown license
-
-aws-encryption-sdk under Apache Software License (Apache License 2.0)
-
-jsonpath-ng under Apache Software License (Apache 2.0)
\ No newline at end of file
diff --git a/Pipfile b/Pipfile
index 36b53484..afc2a98f 100644
--- a/Pipfile
+++ b/Pipfile
@@ -4,35 +4,23 @@ verify_ssl = true
name = "pypi"
[packages]
-aws_lambda_powertools = {extras=["tracer", "validation"], version=">=2.4.0"}
-requests = ">=2.28.1"
-urllib3 = "<2"
[dev-packages]
-aws-cdk-lib = ">=2.63.2"
boto3 = ">=1.26.0"
-boto3-stubs = {extras = ["essential", "proton"], version = "*"}
-cdk-nag = "*"
+boto3-stubs = {extras = ["essential", "dynamodb", "iot"], version = "*"}
jinja2 = "*"
markdown-to-json = "*"
mypy = "*"
pre-commit = "*"
pycln = "*"
pylint = "*"
-pytest = "*"
-pytest-cov = "*"
-pytest-mock = "*"
-syrupy = "*"
-toml = "*"
-types-boto3 = "*"
+requests = ">=2.28.1"
+types-boto3 = ">=1.0.2"
+types-pyyaml = "*"
types-python-dateutil = "*"
-types-python-jose = "*"
types-requests = ">=2.28.1"
types-setuptools = "*"
-types-urllib3 = "*"
-types-toml = "*"
-wrapt = "*"
-freezegun="*"
+wheel = "*"
[requires]
python_version = "3.10"
diff --git a/Pipfile.lock b/Pipfile.lock
index 7e6a1abb..30657b4b 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "1f8b74160411bf2b99402c570959fcec6225dc983317d20df4a9a59015797d33"
+ "sha256": "481752b6bcd2ca09e82d8ee220d3558288370ef8b9d7e9545fd7258b9d87660d"
},
"pipfile-spec": 6,
"requires": {
@@ -15,388 +15,61 @@
}
]
},
- "default": {
- "aws-lambda-powertools": {
- "extras": [
- "tracer",
- "validation"
- ],
- "hashes": [
- "sha256:3860609ad279f9c00c0300d8d724b82e0555638351938292629367b229f3550a",
- "sha256:dce14cb7aa7aaa34b790f7721ac2ef4525d684680b008bf8cb1b3e7a360ebfd0"
- ],
- "markers": "python_full_version >= '3.7.4' and python_full_version < '4.0.0'",
- "version": "==2.26.0"
- },
- "aws-xray-sdk": {
- "hashes": [
- "sha256:0bbfdbc773cfef4061062ac940b85e408297a2242f120bcdfee2593209b1e432",
- "sha256:f6803832dc08d18cc265e2327a69bfa9ee41c121fac195edc9745d04b7a566c3"
- ],
- "version": "==2.12.1"
- },
- "botocore": {
- "hashes": [
- "sha256:90716c6f1af97e5c2f516e9a3379767ebdddcc6cbed79b026fa5038ce4e5e43e",
- "sha256:f74e3da98dfcec17bc63ef58f82c643bf5bd7ec6cc11a26ede21cc4cd064917f"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.31.65"
- },
- "certifi": {
- "hashes": [
- "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
- "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==2023.7.22"
- },
- "charset-normalizer": {
- "hashes": [
- "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843",
- "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786",
- "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e",
- "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8",
- "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4",
- "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa",
- "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d",
- "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82",
- "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7",
- "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895",
- "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d",
- "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a",
- "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382",
- "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678",
- "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b",
- "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e",
- "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741",
- "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4",
- "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596",
- "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9",
- "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69",
- "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c",
- "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77",
- "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13",
- "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459",
- "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e",
- "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7",
- "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908",
- "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a",
- "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f",
- "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8",
- "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482",
- "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d",
- "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d",
- "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545",
- "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34",
- "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86",
- "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6",
- "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe",
- "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e",
- "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc",
- "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7",
- "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd",
- "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c",
- "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557",
- "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a",
- "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89",
- "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078",
- "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e",
- "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4",
- "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403",
- "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0",
- "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89",
- "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115",
- "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9",
- "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05",
- "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a",
- "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec",
- "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56",
- "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38",
- "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479",
- "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c",
- "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e",
- "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd",
- "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186",
- "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455",
- "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c",
- "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65",
- "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78",
- "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287",
- "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df",
- "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43",
- "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1",
- "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7",
- "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989",
- "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a",
- "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63",
- "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884",
- "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649",
- "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810",
- "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828",
- "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4",
- "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2",
- "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd",
- "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5",
- "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe",
- "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293",
- "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e",
- "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e",
- "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"
- ],
- "markers": "python_full_version >= '3.7.0'",
- "version": "==3.3.0"
- },
- "fastjsonschema": {
- "hashes": [
- "sha256:06dc8680d937628e993fa0cd278f196d20449a1adc087640710846b324d422ea",
- "sha256:aec6a19e9f66e9810ab371cc913ad5f4e9e479b63a7072a2cd060a9369e329a8"
- ],
- "version": "==2.18.1"
- },
- "idna": {
- "hashes": [
- "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
- "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==3.4"
- },
- "jmespath": {
- "hashes": [
- "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
- "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.0.1"
- },
- "python-dateutil": {
- "hashes": [
- "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
- "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.8.2"
- },
- "requests": {
- "hashes": [
- "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
- "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.7'",
- "version": "==2.31.0"
- },
- "six": {
- "hashes": [
- "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
- "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.16.0"
- },
- "typing-extensions": {
- "hashes": [
- "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
- "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==4.8.0"
- },
- "urllib3": {
- "hashes": [
- "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07",
- "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"
- ],
- "index": "pypi",
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
- "version": "==1.26.18"
- },
- "wrapt": {
- "hashes": [
- "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0",
- "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420",
- "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a",
- "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c",
- "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079",
- "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923",
- "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f",
- "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1",
- "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8",
- "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86",
- "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0",
- "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364",
- "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e",
- "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c",
- "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e",
- "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c",
- "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727",
- "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff",
- "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e",
- "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29",
- "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7",
- "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72",
- "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475",
- "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a",
- "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317",
- "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2",
- "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd",
- "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640",
- "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98",
- "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248",
- "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e",
- "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d",
- "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec",
- "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1",
- "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e",
- "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9",
- "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92",
- "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb",
- "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094",
- "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46",
- "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29",
- "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd",
- "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705",
- "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8",
- "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975",
- "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb",
- "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e",
- "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b",
- "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418",
- "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019",
- "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1",
- "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba",
- "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6",
- "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2",
- "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3",
- "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7",
- "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752",
- "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416",
- "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f",
- "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1",
- "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc",
- "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145",
- "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee",
- "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a",
- "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7",
- "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b",
- "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653",
- "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0",
- "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90",
- "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29",
- "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6",
- "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034",
- "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09",
- "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559",
- "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==1.15.0"
- }
- },
+ "default": {},
"develop": {
"astroid": {
"hashes": [
- "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca",
- "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e"
+ "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819",
+ "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"
],
"markers": "python_full_version >= '3.8.0'",
- "version": "==3.0.1"
- },
- "attrs": {
- "hashes": [
- "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
- "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==23.1.0"
- },
- "aws-cdk-lib": {
- "hashes": [
- "sha256:8f806e7d98d54f9c563d199f608b70989ab7e2cd8d0335b6a21af0b022f34d39",
- "sha256:ccd71da043868292c06ef592dd1729fd77c83188240639eec88e561fd2f112b8"
- ],
- "index": "pypi",
- "markers": "python_version ~= '3.7'",
- "version": "==2.101.1"
- },
- "aws-cdk.asset-awscli-v1": {
- "hashes": [
- "sha256:af4d67ef7aa4183073e63be5f88d1ce1912b24d2ebac35148e84678d674bdfcd",
- "sha256:ed1b881402b255daec151e386581a627ce13f4d5cb94b7184e6efc38d27584b0"
- ],
- "markers": "python_version ~= '3.7'",
- "version": "==2.2.200"
- },
- "aws-cdk.asset-kubectl-v20": {
- "hashes": [
- "sha256:346283e43018a43e3b3ca571de3f44e85d49c038dc20851894cb8f9b2052b164",
- "sha256:7f0617ab6cb942b066bd7174bf3e1f377e57878c3e1cddc21d6b2d13c92d0cc1"
- ],
- "markers": "python_version ~= '3.7'",
- "version": "==2.1.2"
- },
- "aws-cdk.asset-node-proxy-agent-v6": {
- "hashes": [
- "sha256:42cdbc1de2ed3f845e3eb883a72f58fc7e5554c2e0b6fcdb366c159778dce74d",
- "sha256:e442673d4f93137ab165b75386761b1d46eea25fc5015e5145ae3afa9da06b6e"
- ],
- "markers": "python_version ~= '3.7'",
- "version": "==2.0.1"
+ "version": "==3.1.0"
},
"boto3": {
"hashes": [
- "sha256:9d52a1605657aeb5b19b09cfc01d9a92f88a616a5daf5479a59656d6341ea6b3",
- "sha256:ff3d0116e0ca6c096547652390025780eace3a28f6c04c9ffbf38448f1e5a87b"
+ "sha256:8b3f5cc7fbedcbb22271c328039df8a6ab343001e746e0cdb24774c426cadcf8",
+ "sha256:f201b6a416f809283d554c652211eecec9fe3a52ed4063dab3f3e7aea7571d9c"
],
"index": "pypi",
- "markers": "python_version >= '3.7'",
- "version": "==1.28.65"
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.54"
},
"boto3-stubs": {
"extras": [
+ "dynamodb",
"essential",
- "proton"
+ "iot"
],
"hashes": [
- "sha256:26bd79d43f4e65512f7226994cba9a60a59e52526d1c59ef62eae9fadaa71e6a",
- "sha256:ce29db1fd5f5ce5088018fd3cc9f3676223ced485743c4d0748b0da0348006aa"
+ "sha256:7db5194e47f76e0010cd00b6ad9725db114d6a3fd04e52ceed3ef1181fe326bc",
+ "sha256:c7b2e8b99f4896cf1226df47d4badaaa8df7426008c96a428bf00205695669e9"
],
- "markers": "python_version >= '3.7'",
- "version": "==1.28.65"
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.54"
},
"botocore": {
"hashes": [
- "sha256:90716c6f1af97e5c2f516e9a3379767ebdddcc6cbed79b026fa5038ce4e5e43e",
- "sha256:f74e3da98dfcec17bc63ef58f82c643bf5bd7ec6cc11a26ede21cc4cd064917f"
+ "sha256:4061ff4be3efcf53547ebadf2c94d419dfc8be7beec24e9fa1819599ffd936fa",
+ "sha256:bf215d93e9d5544c593962780d194e74c6ee40b883d0b885e62ef35fc0ec01e5"
],
- "markers": "python_version >= '3.7'",
- "version": "==1.31.65"
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.54"
},
"botocore-stubs": {
"hashes": [
- "sha256:466d448eb4da3e808999b8cb2eabdc3d8c6f851b017ab06af48a598a2443082d",
- "sha256:a923f0f1fceec68affcf878be3d2af906763d68dce95a9562c4c3a529834167e"
+ "sha256:958f0084322dc9e549f73151b686fa51b15858fb2b3a573b9f4367f073fff463",
+ "sha256:bcc35bfbd14d1261813681c40108f2ce85fdf082c15b0a04016d3c22dd93b73f"
],
- "markers": "python_version >= '3.7' and python_version < '4.0'",
- "version": "==1.31.65"
+ "markers": "python_version >= '3.8' and python_version < '4.0'",
+ "version": "==1.34.54"
},
- "cattrs": {
- "hashes": [
- "sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4",
- "sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==23.1.2"
- },
- "cdk-nag": {
+ "certifi": {
"hashes": [
- "sha256:99e6199f5bf9b8637f1a9c6df4bbfb46b66be3faed163e4cae16bd23fbb187dc",
- "sha256:9ac2299d96049e3c2db4f9dc784703e8a7396e0aa69f8e898c32fa60f6d6cebc"
+ "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
+ "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
],
- "index": "pypi",
- "markers": "python_version ~= '3.7'",
- "version": "==2.27.165"
+ "markers": "python_version >= '3.6'",
+ "version": "==2024.2.2"
},
"cfgv": {
"hashes": [
@@ -406,6 +79,102 @@
"markers": "python_version >= '3.8'",
"version": "==3.4.0"
},
+ "charset-normalizer": {
+ "hashes": [
+ "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027",
+ "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087",
+ "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786",
+ "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8",
+ "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09",
+ "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185",
+ "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574",
+ "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e",
+ "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519",
+ "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898",
+ "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269",
+ "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3",
+ "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f",
+ "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6",
+ "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8",
+ "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a",
+ "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73",
+ "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc",
+ "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714",
+ "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2",
+ "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc",
+ "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce",
+ "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d",
+ "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e",
+ "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6",
+ "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269",
+ "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96",
+ "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d",
+ "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a",
+ "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4",
+ "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77",
+ "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d",
+ "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0",
+ "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed",
+ "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068",
+ "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac",
+ "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25",
+ "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8",
+ "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab",
+ "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26",
+ "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2",
+ "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db",
+ "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f",
+ "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5",
+ "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99",
+ "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c",
+ "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d",
+ "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811",
+ "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa",
+ "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a",
+ "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03",
+ "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b",
+ "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04",
+ "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c",
+ "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001",
+ "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458",
+ "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389",
+ "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99",
+ "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985",
+ "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537",
+ "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238",
+ "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f",
+ "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d",
+ "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796",
+ "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a",
+ "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143",
+ "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8",
+ "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c",
+ "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5",
+ "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5",
+ "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711",
+ "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4",
+ "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6",
+ "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c",
+ "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7",
+ "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4",
+ "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b",
+ "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae",
+ "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12",
+ "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c",
+ "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae",
+ "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8",
+ "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887",
+ "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b",
+ "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4",
+ "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f",
+ "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5",
+ "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33",
+ "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519",
+ "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==3.3.2"
+ },
"click": {
"hashes": [
"sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
@@ -414,155 +183,61 @@
"markers": "python_version >= '3.7'",
"version": "==8.1.7"
},
- "constructs": {
- "hashes": [
- "sha256:2972f514837565ff5b09171cfba50c0159dfa75ee86a42921ea8c86f2941b3d2",
- "sha256:518551135ec236f9cc6b86500f4fbbe83b803ccdc6c2cb7684e0b7c4d234e7b1"
- ],
- "markers": "python_version ~= '3.7'",
- "version": "==10.3.0"
- },
- "coverage": {
- "extras": [
- "toml"
- ],
- "hashes": [
- "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1",
- "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63",
- "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9",
- "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312",
- "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3",
- "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb",
- "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25",
- "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92",
- "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda",
- "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148",
- "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6",
- "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216",
- "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a",
- "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640",
- "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836",
- "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c",
- "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f",
- "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2",
- "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901",
- "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed",
- "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a",
- "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074",
- "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc",
- "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84",
- "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083",
- "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f",
- "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c",
- "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c",
- "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637",
- "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2",
- "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82",
- "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f",
- "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce",
- "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef",
- "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f",
- "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611",
- "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c",
- "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76",
- "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9",
- "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce",
- "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9",
- "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf",
- "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf",
- "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9",
- "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6",
- "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2",
- "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a",
- "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a",
- "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf",
- "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738",
- "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a",
- "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==7.3.2"
- },
"dill": {
"hashes": [
- "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e",
- "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"
+ "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca",
+ "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"
],
"markers": "python_version < '3.11'",
- "version": "==0.3.7"
+ "version": "==0.3.8"
},
"distlib": {
"hashes": [
- "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057",
- "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"
+ "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784",
+ "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"
],
- "version": "==0.3.7"
- },
- "exceptiongroup": {
- "hashes": [
- "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
- "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
- ],
- "markers": "python_version < '3.11'",
- "version": "==1.1.3"
+ "version": "==0.3.8"
},
"filelock": {
"hashes": [
- "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4",
- "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"
+ "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e",
+ "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"
],
"markers": "python_version >= '3.8'",
- "version": "==3.12.4"
- },
- "freezegun": {
- "hashes": [
- "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446",
- "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.6'",
- "version": "==1.2.2"
+ "version": "==3.13.1"
},
"identify": {
"hashes": [
- "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54",
- "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2.5.30"
- },
- "importlib-resources": {
- "hashes": [
- "sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9",
- "sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83"
+ "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791",
+ "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"
],
"markers": "python_version >= '3.8'",
- "version": "==6.1.0"
+ "version": "==2.5.35"
},
- "iniconfig": {
+ "idna": {
"hashes": [
- "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
- "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
+ "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
+ "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
],
- "markers": "python_version >= '3.7'",
- "version": "==2.0.0"
+ "markers": "python_version >= '3.5'",
+ "version": "==3.6"
},
"isort": {
"hashes": [
- "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504",
- "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"
+ "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109",
+ "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"
],
"markers": "python_full_version >= '3.8.0'",
- "version": "==5.12.0"
+ "version": "==5.13.2"
},
"jinja2": {
"hashes": [
- "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
- "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
+ "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
+ "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
- "version": "==3.1.2"
+ "version": "==3.1.3"
},
"jmespath": {
"hashes": [
@@ -572,50 +247,36 @@
"markers": "python_version >= '3.7'",
"version": "==1.0.1"
},
- "jsii": {
- "hashes": [
- "sha256:2fcc68d8cf88260bc8e502789d43ab46e7672b6f82d498ed62a52a4366fbccc5",
- "sha256:e8a9a94c5116da96f11e79f16d4a290e1e7e1652b4addb8cce5c56f8ef570479"
- ],
- "markers": "python_version ~= '3.7'",
- "version": "==1.90.0"
- },
"libcst": {
"hashes": [
- "sha256:003e5e83a12eed23542c4ea20fdc8de830887cc03662432bb36f84f8c4841b81",
- "sha256:0acbacb9a170455701845b7e940e2d7b9519db35a86768d86330a0b0deae1086",
- "sha256:0bf69cbbab5016d938aac4d3ae70ba9ccb3f90363c588b3b97be434e6ba95403",
- "sha256:2d37326bd6f379c64190a28947a586b949de3a76be00176b0732c8ee87d67ebe",
- "sha256:3a07ecfabbbb8b93209f952a365549e65e658831e9231649f4f4e4263cad24b1",
- "sha256:3ebbb9732ae3cc4ae7a0e97890bed0a57c11d6df28790c2b9c869f7da653c7c7",
- "sha256:4bc745d0c06420fe2644c28d6ddccea9474fb68a2135904043676deb4fa1e6bc",
- "sha256:5297a16e575be8173185e936b7765c89a3ca69d4ae217a4af161814a0f9745a7",
- "sha256:5f1cd308a4c2f71d5e4eec6ee693819933a03b78edb2e4cc5e3ad1afd5fb3f07",
- "sha256:63f75656fd733dc20354c46253fde3cf155613e37643c3eaf6f8818e95b7a3d1",
- "sha256:73c086705ed34dbad16c62c9adca4249a556c1b022993d511da70ea85feaf669",
- "sha256:75816647736f7e09c6120bdbf408456f99b248d6272277eed9a58cf50fb8bc7d",
- "sha256:78b7a38ec4c1c009ac39027d51558b52851fb9234669ba5ba62283185963a31c",
- "sha256:7ccaf53925f81118aeaadb068a911fac8abaff608817d7343da280616a5ca9c1",
- "sha256:82d1271403509b0a4ee6ff7917c2d33b5a015f44d1e208abb1da06ba93b2a378",
- "sha256:8ae11eb1ea55a16dc0cdc61b41b29ac347da70fec14cc4381248e141ee2fbe6c",
- "sha256:8afb6101b8b3c86c5f9cec6b90ab4da16c3c236fe7396f88e8b93542bb341f7c",
- "sha256:8c1f2da45f1c45634090fd8672c15e0159fdc46853336686959b2d093b6e10fa",
- "sha256:97fbc73c87e9040e148881041fd5ffa2a6ebf11f64b4ccb5b52e574b95df1a15",
- "sha256:99fdc1929703fd9e7408aed2e03f58701c5280b05c8911753a8d8619f7dfdda5",
- "sha256:9dffa1795c2804d183efb01c0f1efd20a7831db6a21a0311edf90b4100d67436",
- "sha256:bca1841693941fdd18371824bb19a9702d5784cd347cb8231317dbdc7062c5bc",
- "sha256:c653d9121d6572d8b7f8abf20f88b0a41aab77ff5a6a36e5a0ec0f19af0072e8",
- "sha256:c8f26250f87ca849a7303ed7a4fd6b2c7ac4dec16b7d7e68ca6a476d7c9bfcdb",
- "sha256:cc9b6ac36d7ec9db2f053014ea488086ca2ed9c322be104fbe2c71ca759da4bb",
- "sha256:d22d1abfe49aa60fc61fa867e10875a9b3024ba5a801112f4d7ba42d8d53242e",
- "sha256:d68c34e3038d3d1d6324eb47744cbf13f2c65e1214cf49db6ff2a6603c1cd838",
- "sha256:e3d8cf974cfa2487b28f23f56c4bff90d550ef16505e58b0dca0493d5293784b",
- "sha256:f36f592e035ef84f312a12b75989dde6a5f6767fe99146cdae6a9ee9aff40dd0",
- "sha256:f561c9a84eca18be92f4ad90aa9bd873111efbea995449301719a1a7805dbc5c",
- "sha256:fe41b33aa73635b1651f64633f429f7aa21f86d2db5748659a99d9b7b1ed2a90"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.1.0"
+ "sha256:0cb92398236566f0b73a0c73f8a41a9c4906c793e8f7c2745f30e3fb141a34b5",
+ "sha256:13ca9fe82326d82feb2c7b0f5a320ce7ed0d707c32919dd36e1f40792459bf6f",
+ "sha256:1b5fecb2b26fa3c1efe6e05ef1420522bd31bb4dae239e4c41fdf3ddbd853aeb",
+ "sha256:1d45718f7e7a1405a16fd8e7fc75c365120001b6928bfa3c4112f7e533990b9a",
+ "sha256:2bbb4e442224da46b59a248d7d632ed335eae023a921dea1f5c72d2a059f6be9",
+ "sha256:38fbd56f885e1f77383a6d1d798a917ffbc6d28dc6b1271eddbf8511c194213e",
+ "sha256:3c7c0edfe3b878d64877671261c7b3ffe9d23181774bfad5d8fcbdbbbde9f064",
+ "sha256:4973a9d509cf1a59e07fac55a98f70bc4fd35e09781dffb3ec93ee32fc0de7af",
+ "sha256:5c0d548d92c6704bb07ce35d78c0e054cdff365def0645c1b57c856c8e112bb4",
+ "sha256:5e54389abdea995b39ee96ad736ed1b0b8402ed30a7956b7a279c10baf0c0294",
+ "sha256:6dd388c74c04434b41e3b25fc4a0fafa3e6abf91f97181df55e8f8327fd903cc",
+ "sha256:71dd69fff76e7edaf8fae0f63ffcdbf5016e8cd83165b1d0688d6856aa48186a",
+ "sha256:7f4919978c2b395079b64d8a654357854767adbabab13998b39c1f0bc67da8a7",
+ "sha256:82373a35711a8bb2a664dba2b7aeb20bbcce92a4db40af964e9cb2b976f989e7",
+ "sha256:8b56130f18aca9a98b3bcaf5962b2b26c2dcdd6d5132decf3f0b0b635f4403ba",
+ "sha256:968b93400e66e6711a29793291365e312d206dbafd3fc80219cfa717f0f01ad5",
+ "sha256:b4066dcadf92b183706f81ae0b4342e7624fc1d9c5ca2bf2b44066cb74bf863f",
+ "sha256:ba24b8cf789db6b87c6e23a6c6365f5f73cb7306d929397581d5680149e9990c",
+ "sha256:c0149d24a455536ff2e41b3a48b16d3ebb245e28035013c91bd868def16592a0",
+ "sha256:c80f36f4a02d530e28eac7073aabdea7c6795fc820773a02224021d79d164e8b",
+ "sha256:dded0e4f2e18150c4b07fedd7ef84a9abc7f9bd2d47cc1c485248ee1ec58e5cc",
+ "sha256:dece0362540abfc39cd2cf5c98cde238b35fd74a1b0167e2563e4b8bb5f47489",
+ "sha256:e01879aa8cd478bb8b1e4285cfd0607e64047116f7ab52bc2a787cde584cd686",
+ "sha256:f080e9af843ff609f8f35fc7275c8bf08b02c31115e7cd5b77ca3b6a56c75096",
+ "sha256:f2342634f6c61fc9076dc0baf21e9cf5ef0195a06e1e95c0c9dc583ba3a30d00"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==1.2.0"
},
"markdown-to-json": {
"hashes": [
@@ -628,69 +289,69 @@
},
"markupsafe": {
"hashes": [
- "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e",
- "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e",
- "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431",
- "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686",
- "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c",
- "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559",
- "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc",
- "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb",
- "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939",
- "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c",
- "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0",
- "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4",
- "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9",
- "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575",
- "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba",
- "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d",
- "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd",
- "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3",
- "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00",
- "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155",
- "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac",
- "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52",
- "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f",
- "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8",
- "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b",
- "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007",
- "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24",
- "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea",
- "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198",
- "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0",
- "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee",
- "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be",
- "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2",
- "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1",
- "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707",
- "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6",
- "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c",
- "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58",
- "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823",
- "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779",
- "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636",
- "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c",
- "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad",
- "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee",
- "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc",
- "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2",
- "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48",
- "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7",
- "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e",
- "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b",
- "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa",
- "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5",
- "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e",
- "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb",
- "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9",
- "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57",
- "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc",
- "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc",
- "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2",
- "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"
+ "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
+ "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
+ "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
+ "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
+ "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
+ "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
+ "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
+ "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
+ "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
+ "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
+ "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
+ "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
+ "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
+ "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
+ "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
+ "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
+ "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
+ "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
+ "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
+ "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
+ "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
+ "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
+ "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
+ "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
+ "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
+ "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
+ "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
+ "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
+ "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
+ "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
+ "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
+ "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
+ "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
+ "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
+ "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
+ "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
+ "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
+ "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
+ "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
+ "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
+ "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
+ "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
+ "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
+ "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
+ "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
+ "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
+ "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
+ "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
+ "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
+ "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
+ "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
+ "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
+ "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
+ "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
+ "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
+ "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
+ "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
+ "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
+ "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
+ "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
],
"markers": "python_version >= '3.7'",
- "version": "==2.1.3"
+ "version": "==2.1.5"
},
"mccabe": {
"hashes": [
@@ -702,93 +363,93 @@
},
"mypy": {
"hashes": [
- "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7",
- "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e",
- "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c",
- "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169",
- "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208",
- "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0",
- "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1",
- "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1",
- "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7",
- "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45",
- "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143",
- "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5",
- "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f",
- "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd",
- "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245",
- "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f",
- "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332",
- "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30",
- "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183",
- "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f",
- "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85",
- "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46",
- "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71",
- "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660",
- "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb",
- "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c",
- "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"
+ "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6",
+ "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d",
+ "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02",
+ "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d",
+ "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3",
+ "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3",
+ "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3",
+ "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66",
+ "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259",
+ "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835",
+ "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd",
+ "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d",
+ "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8",
+ "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07",
+ "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b",
+ "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e",
+ "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6",
+ "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae",
+ "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9",
+ "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d",
+ "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a",
+ "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592",
+ "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218",
+ "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817",
+ "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4",
+ "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410",
+ "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==1.6.1"
+ "version": "==1.8.0"
},
"mypy-boto3-cloudformation": {
"hashes": [
- "sha256:b353d52a5607c54d2916f4bde26e9be90920635beb9ffb9255cd862dca3b56bf",
- "sha256:f5c9012d7fbf9c39bb314ac192e14115dbca9495e364479a16e1fa21cac23d78"
+ "sha256:49d04c090dae3fd8289738ae592cac9d6faa5169684de40c2730b425bba2a32d",
+ "sha256:bfe5ec405eae6dae31dc9874729eef5e668e634eae8972032f00400d17bd2c7d"
],
- "version": "==1.28.64"
+ "version": "==1.34.32"
},
"mypy-boto3-dynamodb": {
"hashes": [
- "sha256:a3039f8ada07a218f97f0c70a82ed9cf461a0cb5133194fcf1e0e87b15c899a5",
- "sha256:c4c16a00e90db5857cbeee207f6dec954ca142bd52e2de0f3d52be6d50d83d16"
+ "sha256:126da0a29ca48502cfa9a26e3024341233d8419f7e03273cea17af7d38e724bd",
+ "sha256:1af7c80a0891edac29e5b70441122f6803eb772a3b7b498396eec30368232541"
],
- "version": "==1.28.55"
+ "version": "==1.34.46"
},
"mypy-boto3-ec2": {
"hashes": [
- "sha256:0871b8875956c05b3020941a183e71099d8da10baf30b127d7b22aebf29c93a8",
- "sha256:807e0508bb4ae9baf1561eac07ffdb951dfd5b7171586f8220898b0c7dc2e2ef"
+ "sha256:ce34c2d7741be1918caf5b46cafb0cb7b1f6ac81ec6fbd8846bbe85c93d43101",
+ "sha256:f36180ea33bad6626ff5302def1250eeb6612fafa15a56d269190d33d5a42093"
],
- "version": "==1.28.63"
+ "version": "==1.34.54"
},
- "mypy-boto3-lambda": {
+ "mypy-boto3-iot": {
"hashes": [
- "sha256:7cbbee5560f347548a8f43324b31b2abfa1f56ec7380f20dadb837533fc0552a",
- "sha256:bcfc747594704664d41fb904f59e4173c718d1bffc92555fc9ca57f8c4b1b970"
+ "sha256:6161a8b4e3ca96363807424bd48f9ac64e0c259224f38ad5c6866ef6dcc11acb",
+ "sha256:825f93f6042def95281608a7df104484ab7b3f0a8af867d1f133e724467f9c8f"
],
- "version": "==1.28.63"
+ "version": "==1.34.52"
},
- "mypy-boto3-proton": {
+ "mypy-boto3-lambda": {
"hashes": [
- "sha256:4c64b1a65311e8f4094fc2ce4cd51cb785630c032ab600648371ce381f13a3bc",
- "sha256:cd675cc3ccf425a931b8e26ad1fffaa3f0a0cacda1e92bd66613db228a72234f"
+ "sha256:275297944c5e36a170b37ce70229f21db6dd3561606799f18d96e36ac5df6876",
+ "sha256:a12232002e04ee06b413b47068bc6bb085aeaa3693d28e9bf0efd76fa6953a0b"
],
- "version": "==1.28.36"
+ "version": "==1.34.46"
},
"mypy-boto3-rds": {
"hashes": [
- "sha256:1627f3944bd562997a0705e5d50f12301fdc9d84aa0120cd630e8f9579c07d41",
- "sha256:af581b770609fb307f537e43fd3cc6e293bebc0acc8e3a53dfae2035e3dd5f29"
+ "sha256:59124bd98653c73c685b7dc0d0a9069572d340f0ecb116a9706aa3e2d40a166d",
+ "sha256:9561dfac562ec9cd039806d5de2bc2bb8be4f9f7c03620270550a49e456fef46"
],
- "version": "==1.28.63"
+ "version": "==1.34.50"
},
"mypy-boto3-s3": {
"hashes": [
- "sha256:11a3db97398973d4ae28489b94c010778a0a5c65f99e00268456c3fea67eca79",
- "sha256:b008809f448e74075012d4fc54b0176de0b4f49bc38e39de30ca0e764eb75056"
+ "sha256:71c39ab0623cdb442d225b71c1783f6a513cff4c4a13505a2efbb2e3aff2e965",
+ "sha256:f9669ecd182d5bf3532f5f2dcc5e5237776afe157ad5a0b37b26d6bec5fcc432"
],
- "version": "==1.28.55"
+ "version": "==1.34.14"
},
"mypy-boto3-sqs": {
"hashes": [
- "sha256:8457aa9f2a6da44e8543e547597773f67a04e517f6a398989117cf1fa3f70d6e",
- "sha256:d9c159e020f0ef225a6d5850a3673e8b236327243ba5ffe0d13762ae4fdc0e21"
+ "sha256:0bf8995f58919ab295398100e72eaa7da898adcfd9d339a42f3c48ce473419d5",
+ "sha256:94d8aea4ae75605f70e58e440d706e04d5c614101ddb2f0c73d306d776d10995"
],
- "version": "==1.28.36"
+ "version": "==1.34.0"
},
"mypy-extensions": {
"hashes": [
@@ -806,106 +467,56 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
"version": "==1.8.0"
},
- "packaging": {
- "hashes": [
- "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
- "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==23.2"
- },
"pathspec": {
"hashes": [
- "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20",
- "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"
+ "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
+ "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
],
- "markers": "python_version >= '3.7'",
- "version": "==0.11.2"
+ "markers": "python_version >= '3.8'",
+ "version": "==0.12.1"
},
"platformdirs": {
"hashes": [
- "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3",
- "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==3.11.0"
- },
- "pluggy": {
- "hashes": [
- "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12",
- "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"
+ "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068",
+ "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"
],
"markers": "python_version >= '3.8'",
- "version": "==1.3.0"
+ "version": "==4.2.0"
},
"pre-commit": {
"hashes": [
- "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32",
- "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"
+ "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c",
+ "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"
],
"index": "pypi",
- "markers": "python_version >= '3.8'",
- "version": "==3.5.0"
- },
- "publication": {
- "hashes": [
- "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6",
- "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"
- ],
- "version": "==0.0.3"
+ "markers": "python_version >= '3.9'",
+ "version": "==3.6.2"
},
"pycln": {
"hashes": [
- "sha256:8759b36753234c8f95895a31dde329479ffed2218f49d1a1c77c7edccc02e09b",
- "sha256:d6731e17a60728b827211de2ca4bfc9b40ea1df99a12f3e0fd06a98a0c9e6caa"
+ "sha256:1f3eefb7be18a9ee06c3bdd0ba2e91218cd39317e20130325f107e96eb84b9f6",
+ "sha256:d1bf648df17077306100815d255d45430035b36f66bac635df04a323c61ba126"
],
"index": "pypi",
- "markers": "python_version < '4' and python_full_version >= '3.6.2'",
- "version": "==2.3.0"
+ "markers": "python_version < '4' and python_full_version >= '3.7.0'",
+ "version": "==2.4.0"
},
"pylint": {
"hashes": [
- "sha256:81c6125637be216b4652ae50cc42b9f8208dfb725cdc7e04c48f6902f4dbdf40",
- "sha256:9c90b89e2af7809a1697f6f5f93f1d0e518ac566e2ac4d2af881a69c13ad01ea"
+ "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74",
+ "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"
],
"index": "pypi",
"markers": "python_full_version >= '3.8.0'",
- "version": "==3.0.1"
- },
- "pytest": {
- "hashes": [
- "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002",
- "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.7'",
- "version": "==7.4.2"
- },
- "pytest-cov": {
- "hashes": [
- "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6",
- "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.7'",
- "version": "==4.1.0"
- },
- "pytest-mock": {
- "hashes": [
- "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39",
- "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.7'",
- "version": "==3.11.1"
+ "version": "==3.1.0"
},
"python-dateutil": {
"hashes": [
- "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
- "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
+ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.8.2"
+ "version": "==2.9.0.post0"
},
"pyyaml": {
"hashes": [
@@ -938,6 +549,7 @@
"sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
"sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
"sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+ "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
"sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
"sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
"sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
@@ -963,21 +575,30 @@
"markers": "python_version >= '3.6'",
"version": "==6.0.1"
},
- "s3transfer": {
+ "requests": {
"hashes": [
- "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a",
- "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e"
+ "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
+ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
],
+ "index": "pypi",
"markers": "python_version >= '3.7'",
- "version": "==0.7.0"
+ "version": "==2.31.0"
+ },
+ "s3transfer": {
+ "hashes": [
+ "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e",
+ "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.10.0"
},
"setuptools": {
"hashes": [
- "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",
- "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"
+ "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56",
+ "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"
],
"markers": "python_version >= '3.8'",
- "version": "==68.2.2"
+ "version": "==69.1.1"
},
"six": {
"hashes": [
@@ -987,24 +608,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
- "syrupy": {
- "hashes": [
- "sha256:6e01fccb4cd5ad37ce54e8c265cde068fa9c37b7a0946c603c328e8a38a7330d",
- "sha256:ea6a237ef374bacebbdb4049f73bf48e3dda76eabd4621a6d104d43077529de6"
- ],
- "index": "pypi",
- "markers": "python_version < '4' and python_full_version >= '3.8.1'",
- "version": "==4.5.0"
- },
- "toml": {
- "hashes": [
- "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
- "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
- ],
- "index": "pypi",
- "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==0.10.2"
- },
"tomli": {
"hashes": [
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
@@ -1015,19 +618,11 @@
},
"tomlkit": {
"hashes": [
- "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86",
- "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"
+ "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b",
+ "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"
],
"markers": "python_version >= '3.7'",
- "version": "==0.12.1"
- },
- "typeguard": {
- "hashes": [
- "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4",
- "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"
- ],
- "markers": "python_full_version >= '3.5.3'",
- "version": "==2.13.3"
+ "version": "==0.12.4"
},
"typer": {
"hashes": [
@@ -1039,11 +634,11 @@
},
"types-awscrt": {
"hashes": [
- "sha256:7b55f5a12ccd4407bc8f1e35c69bb40c931f8513ce1ad81a4527fce3989003fd",
- "sha256:9a21caac4287c113dd52665707785c45bb1d3242b7a2b8aeb57c49e9e749a330"
+ "sha256:61811bbf4de95248939f9276a434be93d2b95f6ccfe8aa94e56999e9778cfcc2",
+ "sha256:79d5bfb01f64701b6cf442e89a37d9c4dc6dbb79a46f2f611739b2418d30ecfd"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
- "version": "==0.19.3"
+ "version": "==0.20.5"
},
"types-boto3": {
"hashes": [
@@ -1053,78 +648,56 @@
"index": "pypi",
"version": "==1.0.2"
},
- "types-pyasn1": {
- "hashes": [
- "sha256:4bfea6548206866302885c36aba945c0deaa40898a313112b5cff7f903a56d71",
- "sha256:62f1ba64c9f8975de301014722e154ef1d6097463844de1ed733e719dfc87780"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==0.5.0.0"
- },
"types-python-dateutil": {
"hashes": [
- "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b",
- "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"
+ "sha256:1f8db221c3b98e6ca02ea83a58371b22c374f42ae5bbdf186db9c9a76581459f",
+ "sha256:efbbdc54590d0f16152fa103c9879c7d4a00e82078f6e2cf01769042165acaa2"
],
"index": "pypi",
- "version": "==2.8.19.14"
+ "markers": "python_version >= '3.8'",
+ "version": "==2.8.19.20240106"
},
- "types-python-jose": {
+ "types-pyyaml": {
"hashes": [
- "sha256:3c316675c3cee059ccb9aff87358254344915239fa7f19cee2787155a7db14ac",
- "sha256:95592273443b45dc5cc88f7c56aa5a97725428753fb738b794e63ccb4904954e"
+ "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062",
+ "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"
],
"index": "pypi",
- "version": "==3.3.4.8"
+ "version": "==6.0.12.12"
},
"types-requests": {
"hashes": [
- "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9",
- "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0"
+ "sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b",
+ "sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5"
],
"index": "pypi",
- "markers": "python_version >= '3.7'",
- "version": "==2.31.0.6"
+ "markers": "python_version >= '3.8'",
+ "version": "==2.31.0.20240218"
},
"types-s3transfer": {
"hashes": [
- "sha256:aca0f2486d0a3a5037cd5b8f3e20a4522a29579a8dd183281ff0aa1c4e2c8aa7",
- "sha256:ae9ed9273465d9f43da8b96307383da410c6b59c3b2464c88d20b578768e97c6"
+ "sha256:35e4998c25df7f8985ad69dedc8e4860e8af3b43b7615e940d53c00d413bdc69",
+ "sha256:44fcdf0097b924a9aab1ee4baa1179081a9559ca62a88c807e2b256893ce688f"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
- "version": "==0.7.0"
+ "version": "==0.10.0"
},
"types-setuptools": {
"hashes": [
- "sha256:77edcc843e53f8fc83bb1a840684841f3dc804ec94562623bfa2ea70d5a2ba1b",
- "sha256:a4216f1e2ef29d089877b3af3ab2acf489eb869ccaf905125c69d2dc3932fd85"
+ "sha256:99c1053920a6fa542b734c9ad61849c3993062f80963a4034771626528e192a0",
+ "sha256:ed5462cf8470831d1bdbf300e1eeea876040643bfc40b785109a5857fa7d3c3f"
],
"index": "pypi",
- "version": "==68.2.0.0"
- },
- "types-toml": {
- "hashes": [
- "sha256:58b0781c681e671ff0b5c0319309910689f4ab40e8a2431e205d70c94bb6efb1",
- "sha256:61951da6ad410794c97bec035d59376ce1cbf4453dc9b6f90477e81e4442d631"
- ],
- "index": "pypi",
- "version": "==0.10.8.7"
- },
- "types-urllib3": {
- "hashes": [
- "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f",
- "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"
- ],
- "index": "pypi",
- "version": "==1.26.25.14"
+ "markers": "python_version >= '3.8'",
+ "version": "==69.1.0.20240302"
},
"typing-extensions": {
"hashes": [
- "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
- "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
+ "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
+ "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
],
- "markers": "python_version >= '3.8'",
- "version": "==4.8.0"
+ "markers": "python_version < '3.12'",
+ "version": "==4.10.0"
},
"typing-inspect": {
"hashes": [
@@ -1135,101 +708,28 @@
},
"urllib3": {
"hashes": [
- "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07",
- "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"
+ "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
+ "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
],
- "index": "pypi",
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
- "version": "==1.26.18"
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.7"
},
"virtualenv": {
"hashes": [
- "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b",
- "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"
+ "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a",
+ "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"
],
"markers": "python_version >= '3.7'",
- "version": "==20.24.5"
+ "version": "==20.25.1"
},
- "wrapt": {
+ "wheel": {
"hashes": [
- "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0",
- "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420",
- "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a",
- "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c",
- "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079",
- "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923",
- "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f",
- "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1",
- "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8",
- "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86",
- "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0",
- "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364",
- "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e",
- "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c",
- "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e",
- "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c",
- "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727",
- "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff",
- "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e",
- "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29",
- "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7",
- "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72",
- "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475",
- "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a",
- "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317",
- "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2",
- "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd",
- "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640",
- "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98",
- "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248",
- "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e",
- "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d",
- "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec",
- "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1",
- "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e",
- "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9",
- "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92",
- "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb",
- "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094",
- "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46",
- "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29",
- "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd",
- "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705",
- "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8",
- "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975",
- "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb",
- "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e",
- "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b",
- "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418",
- "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019",
- "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1",
- "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba",
- "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6",
- "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2",
- "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3",
- "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7",
- "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752",
- "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416",
- "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f",
- "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1",
- "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc",
- "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145",
- "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee",
- "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a",
- "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7",
- "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b",
- "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653",
- "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0",
- "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90",
- "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29",
- "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6",
- "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034",
- "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09",
- "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559",
- "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"
+ "sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d",
+ "sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==1.15.0"
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==0.42.0"
}
}
}
diff --git a/README.md b/README.md
index 34c71467..d49368f4 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# Connected Mobility Solution on AWS
-
+
**[Connected Mobility Solution on AWS](https://aws.amazon.com/solutions/implementations/connected-mobility-solution-on-aws/)** | **[🚧 Feature request](https://github.com/aws-solutions/connected-mobility-solution-on-aws/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)** | **[🐛 Bug Report](https://github.com/aws-solutions/connected-mobility-solution-on-aws/issues/new?assignees=&labels=bug&template=bug_report.md&title=)** | **[❓ General Question](https://github.com/aws-solutions/connected-mobility-solution-on-aws/issues/new?assignees=&labels=question&template=general_question.md&title=)**
**Note**: If you want to use the solution without building from source,
@@ -8,22 +8,20 @@ navigate to the [AWS Solution Page](https://aws.amazon.com/solutions/implementat
**If you want to jump straight into building and deploying, [click here](#deployment-prerequisites)**
## Table of Contents
+
- [Connected Mobility Solution on AWS](#connected-mobility-solution-on-aws)
- [Table of Contents](#table-of-contents)
- [Solution Overview](#solution-overview)
- [Architecture Diagrams](#architecture-diagrams)
- - [Solution Architecture Diagram](#solution-architecture-diagram)
- [ACDP Architecture Diagram](#acdp-architecture-diagram)
- - [CMS Backstage Architecture Diagram](#cms-backstage-architecture-diagram)
- [ACDP Deployment Sequence Diagram](#acdp-deployment-sequence-diagram)
- [Module Deployment Sequence Diagram](#module-deployment-sequence-diagram)
- [CMS Modules](#cms-modules)
- - [Environment](#environment)
- - [Deployment Setup/Pre-requisites](#deployment-setuppre-requisites)
- - [Pre-requisite tools](#pre-requisite-tools)
- - [Required Tool Versions](#required-tool-versions)
+ - [Deployment Prerequisites](#deployment-prerequisites)
- [Clone the Repository](#clone-the-repository)
- - [Install Pre-requisite Tools (OSX/Linux)](#install-pre-requisite-tools-osxlinux)
+ - [Required Tools](#required-tools)
+ - [Required Tool Versions](#required-tool-versions)
+ - [Install Required Tools (OSX/Linux)](#install-required-tools-osxlinux)
- [NVM](#nvm)
- [Node / NPM](#node--npm)
- [Yarn](#yarn)
@@ -34,30 +32,27 @@ navigate to the [AWS Solution Page](https://aws.amazon.com/solutions/implementat
- [AWS CDK Toolkit](#aws-cdk-toolkit)
- [Verify Required Tool Installations](#verify-required-tool-installations)
- [Install Solution Dependencies](#install-solution-dependencies)
- - [Create a Route53 Hosted Zone](#create-a-route53-hosted-zone)
- - [Setup environment variables](#setup-environment-variables)
- - [Create a *.env* file (preferred method)](#create-a-env-file-preferred-method)
- - [Set environment variables (secondary option)](#set-environment-variables-secondary-option)
- - [Verify environment variable setup (cdk-context)](#verify-environment-variable-setup-cdk-context)
+ - [Create an Amazon Route 53 Hosted Zone](#create-an-amazon-route-53-hosted-zone)
+ - [Setup Environment Variables](#setup-environment-variables)
- [Deploy](#deploy)
- - [Deployment Pre-Requisites](#deployment-pre-requisites)
- - [Run CDK Bootstrap](#run-cdk-bootstrap)
- - [Upload S3 Deployment Assets](#upload-s3-deployment-assets)
- - [Deploy the Automotive Cloud Developer Portal (ACDP)](#deploy-the-automotive-cloud-developer-portal-acdp)
+ - [Prerequisites](#prerequisites)
+ - [Build the Solution's Modules](#build-the-solutions-modules)
+ - [Upload Assets to S3](#upload-assets-to-s3)
+ - [Deploy on AWS](#deploy-on-aws)
- [Monitoring the ACDP Deployment](#monitoring-the-acdp-deployment)
- - [Bootstrap Proton](#bootstrap-proton)
- [Deploy CMS Modules via Backstage](#deploy-cms-modules-via-backstage)
- [CMS Module Deployment Order](#cms-module-deployment-order)
+ - [Deployment Order of Required CMS Config](#deployment-order-of-required-cms-config)
- [Deployment Order of Modules with Dependencies](#deployment-order-of-modules-with-dependencies)
- - [Modules Without Dependencies](#modules-without-dependencies)
+ - [Modules Without Dependencies After CMS Config](#modules-without-dependencies-after-cms-config)
- [Example Module Deployment via Backstage](#example-module-deployment-via-backstage)
- - [Cost scaling](#cost-scaling)
+ - [Cost Scaling](#cost-scaling)
- [Collection of Operational Metrics](#collection-of-operational-metrics)
- - [Teardown](#teardown)
+ - [Uninstall the Solution](#uninstall-the-solution)
- [Developer Guide](#developer-guide)
- [Logging](#logging)
- - [Lambda functions](#lambda-functions)
- - [Backstage logs](#backstage-logs)
+ - [Lambda Functions](#lambda-functions)
+ - [Backstage Logs](#backstage-logs)
- [Pre-Commit Hooks](#pre-commit-hooks)
- [Unit Test](#unit-test)
- [License](#license)
@@ -67,13 +62,13 @@ navigate to the [AWS Solution Page](https://aws.amazon.com/solutions/implementat
The Connected Mobility Solution (CMS) on AWS provides the automotive
industry customers the capability to communicate between vehicles and the
AWS Cloud, manage and orchestrate CMS on AWS deployments from a centralized
-developer platform, securely authenticate and authorize users and services
+developer platform, securely authenticate and authorize users and services,
onboard vehicles into AWS IoT Core, create vehicle profiles for storing data
-about registered vehicles and capture and store telemetry data emitted from
-registered vehicles. Additionally it provides capabilities to query stored
-vehicle data, create alerts and subscribe to notifications based on data thresholds,
-visualize vehicle telemetry data through a provided dashboard and simulate
-connected vehicle data.
+about registered vehicles, capture and store telemetry data emitted from
+registered vehicles, and consume data from FleetWise campaigns. Additionally
+it provides capabilities to query stored vehicle data, create alerts and subscribe
+to notifications based on data thresholds, visualize vehicle telemetry data through
+a provided dashboard, and simulate connected vehicle data.
For more information and a detailed deployment guide, visit the
[Connected Mobility Solution on AWS](https://aws.amazon.com/solutions/implementations/connected-mobility-solution-on-aws/)
@@ -81,46 +76,52 @@ solution page.
## Architecture Diagrams
-### Solution Architecture Diagram
-
-![Solution Architecture Diagram](./documentation/architecture/diagrams/cms-all-modules-architecture-diagram.svg)
-
### ACDP Architecture Diagram
-![ACDP Architecture Diagram](./documentation/architecture/diagrams/cms-acdp-architecture-diagram.svg)
-
-### CMS Backstage Architecture Diagram
-
-![CMS Backstage Architecture Diagram](./documentation/architecture/diagrams/cms-backstage-architecture-diagram.svg)
+![ACDP Architecture Diagram](source/modules/acdp/documentation/architecture/cms-acdp-deployment-diagram.svg)
### ACDP Deployment Sequence Diagram
-![ACDP Deployment Sequence Diagram](./documentation/sequence/cms-acdp-deployment-sequence-diagram.svg)
+![ACDP Deployment Sequence Diagram](./source/modules/acdp/documentation/sequence/cms-acdp-deployment-sequence-diagram.svg)
### Module Deployment Sequence Diagram
-![Module Deployment Sequence Diagram](./documentation/sequence/cms-module-deployment-sequence-diagram.svg)
+![Module Deployment Sequence Diagram](./source/modules/acdp/backstage/documentation/sequence/cms-module-deployment-sequence-diagram.svg)
## CMS Modules
-For detailed information visit the module's README
+For detailed information visit the modules' README
+
+- [ACDP](./source/modules/acdp/README.md)
+ - [Backstage](./source/modules/acdp/backstage/README.md)
+- [Alerts](./source/modules/cms_alerts/README.md)
+- [API](./source/modules/cms_api/README.md)
+- [Auth](./source/modules/cms_auth/README.md)
+- [Auth Setup](./source/modules/auth_setup/README.md)
+- [Connect & Store](./source/modules/cms_connect_store/README.md)
+- [Config](./source/modules/cms_config/README.md)
+- [EV Battery Health](./source/modules/cms_ev_battery_health/README.md)
+- [Fleetwise Connector](./source/modules/cms_fleetwise_connector/README.md)
+- [Provisioning](./source/modules/cms_provisioning/README.md)
+- [Vehicle Simulator](./source/modules/cms_vehicle_simulator/README.md)
+- [VPC](./source/modules/vpc/README.md)
+
+## Deployment Prerequisites
-- [Alerts](./templates/modules/cms_alerts_on_aws/v1/instance_infrastructure/README.md)
-- [API](./templates/modules/cms_api_on_aws/v1/instance_infrastructure/README.md)
-- [Connect & Store](./templates/modules/cms_connect_store_on_aws/v1/instance_infrastructure/README.md)
-- [EV Battery Health](./templates/modules/cms_ev_battery_health_on_aws/v1/instance_infrastructure/README.md)
-- [Provisioning](./templates/modules/cms_provisioning_on_aws/v1/instance_infrastructure/README.md)
-- [Authentication](./templates/modules/cms_user_authentication_on_aws/v1/instance_infrastructure/README.md)
-- [Vehicle Simulator](./templates/modules/cms_vehicle_simulator_on_aws/v1/instance_infrastructure/README.md)
+### Clone the Repository
-### Environment
+If you have not done so, first clone the repository, and then `cd` into the created directory. If you have
+already cloned the repository, ensure you still `cd` into the solution's directory.
-For reference, there is a proton environment setup with further
-details in its [README](./templates/environments/cms_environment/v1/infrastructure/README.md).
+```bash
+git clone https://github.com/aws-solutions/connected-mobility-solution-on-aws.git
+cd connected-mobility-solution-on-aws/
+```
-## Deployment Setup/Pre-requisites
+> **WARNING:** If you do not `cd` into the solution's directory before installing tools,
+> the correct versions may not be installed.
-### Pre-requisite tools
+### Required Tools
To deploy CMS on AWS, a variety of tools are required. These deploy instructions will install the following to your machine:
@@ -144,23 +145,10 @@ For tools not listed here, stable versions should work appropriately.
| Dependency | Version |
|------------|----------|
-| [NodeJS](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) | 18.17.1 |
-| [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) | 9.6.7 |
-| [Python](https://www.python.org) | 3.10.9 |
-
-### Clone the Repository
-
-If you have not done so, first clone the repository, and then `cd` into the created directory. If you have
-already cloned the repository, ensure you still `cd` into the solution's directory.
-
-```bash
-git clone https://github.com/aws-solutions/connected-mobility-solution-on-aws.git
-cd connected-mobility-solution-on-aws
-```
+| [NodeJS](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) | 18.17.* |
+| [Python](https://www.python.org) | 3.10.* |
-> **WARNING:** If you do not `cd` into the repository before following these instructions, the correct versions may not be installed.
-
-### Install Pre-requisite Tools (OSX/Linux)
+### Install Required Tools (OSX/Linux)
Install the following tools in the order instructed here. Where appropriate, a script has been provided to aid in install.
Otherwise, please visit the installation guide provided by the tool's publisher to ensure a correct installation.
@@ -263,113 +251,68 @@ make verify-required-tools
### Install Solution Dependencies
Now that you have the correct tools, you can install the dependencies used by the solution using `make install`.
-After installing, we will activate the environment which contains the dependencies.
+After installing, activate the environment which contains the dependencies.
```bash
make install
-source ./.venv/bin/activate
```
-### Create a Route53 Hosted Zone
+### Create an Amazon Route 53 Hosted Zone
-To deploy the solution, a Route53 Hosted Zone is required to be setup in your account.
+To deploy the solution, an Amazon Route53 Hosted Zone is required to be setup in your account.
You will provide the domain for this hosted zone in the following step when you setup your environment variables.
This is a manual step. For more details, see
[Working with hosted zones](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html).
-### Setup environment variables
+### Setup Environment Variables
To deploy the solution, a variety of environment variables are required. These environment variables will be used to
-provide the context to your CDK deployment.
-
-- `ROUTE53_BASE_DOMAIN` is optional, if unset the base domain will be assumed to be the same as the `ROUTE53_ZONE_NAME`
-variable. This must be set to a superset of the `ROUTE53_ZONE_NAME` (e.g. *Optional-Sub-Domain.`ROUTE53_ZONE_NAME`*).
- - The Route53 Zone Name can be found from the Route53 Hosted Zone you setup in the previous step. Use the AWS console to find this domain.
-- `BACKSTAGE_TEMPLATE_S3_UPDATE_REFRESH_MINS` should be set to something small such as `1 minute` for development.
-It is recommended to have longer refresh intervals for cost savings in production environments.
-
-#### Create a *.env* file (preferred method)
-
-> **NOTE:** Do not use quotes around values in the *.env* file or else the make commands will fail.
-
-Use the following command to create a *.env* file. Replace the defaults with appropriate values for your deployment.
+provide the values to your deployment. To generate the file which will store these environment variables and
+provide their values, run the following command:
```bash
-cat > .env < **NOTE:** The `ROUTE53_ZONE_NAME` can be found from the Amazon Route53 Hosted Zone you setup in the previous step.
+Use the AWS Management Console to find this domain.
## Deploy
Refer to the [deployment diagram](#architecture-diagrams) for a detailed walk-through of what is deployed.
-### Deployment Pre-Requisites
+### Prerequisites
+
+Ensure you've followed the steps in the previous [deployment prerequisites](#deployment-prerequisites) section.
-Ensure you've followed the steps in the previous [deployment prerequisites](##deployment-prerequisites) section.
-- Prerequisite tools installed. Refer to the [install required tools](#install-required-tools-osxlinux)) section for details.
-- Solution dependencies installed. Refer to the [install solution dependencies](#install-solution-dependencies) sections for details.
-- A Route53 Hosted Zone in the deployment account. Refer to the
+- Prerequisite tools installed. Refer to the [install required tools](#install-required-tools-osxlinux) sections for details.
+- Solution dependencies installed. Refer to the [install solution dependencies](#install-solution-dependencies)
+ section for details.
+- An Amazon Route53 Hosted Zone in the deployment account. Refer to the
[create an Amazon Route 53 Hosted Zone](#create-an-amazon-route-53-hosted-zone) section for details.
- Environment variables set. Refer to the [setup environment variables](#setup-environment-variables) section for details.
-### Run CDK Bootstrap
+### Build the Solution's Modules
-It is safest to run a fresh bootstrap for your AWS CDK toolkit which provides the necessary context for the solution deployment.
-Run the following make command to perform this bootstrap.
+The build target manages dependencies, builds required assets (e.g. packaged lambdas), and creates the
+AWS CloudFormation templates for all modules.
```bash
-make bootstrap
+make build
```
-### Upload S3 Deployment Assets
-- Backstage `template.yaml` files
-- AWS Proton Service Template `.tar` files
+### Upload Assets to S3
-The following command will upload the necessary assets to S3 which allow for the deployment of CMS modules via Backstage.
-This includes the `template.yaml` files used to instruct Backstage, as well as the `.tar` files for each module which provide
-the source code for the Proton service templates setup later.
+The upload target creates the necessary buckets for, and uploads, the global and regional assets.
+It also uploads the Backstage .zip asset.
```bash
-make upload-s3-deployment-assets
+make upload
```
-### Deploy the Automotive Cloud Developer Portal (ACDP)
+### Deploy on AWS
-Running this deployment will first deploy the ACDP, followed by the execution of the Backstage pipeline which will deploy Backstage.
+The deploy target deploys all CMS modules, including the ACDP, in an enforced order.
```bash
make deploy
@@ -378,7 +321,7 @@ make deploy
### Monitoring the ACDP Deployment
After the CDK deployment is completed, browse to [CodePipeline](https://console.aws.amazon.com/codesuite/codepipeline/pipelines)
-in the AWS console and verify that the "Backstage-Pipeline" execution completes successfully.
+in the AWS Management Console and verify that the "Backstage-Pipeline" execution completes successfully.
![Successful CodePipeline Execution](./documentation/images/readme/deployment-codepipeline-success.png)
@@ -387,74 +330,53 @@ After the pipeline has completed, the deployment can be considered successfully
> **NOTE:** It can take up to **10 minutes** after the Backstage pipeline completes for Amazon Cognito's auth domain to become
> available for use with Backstage. If your Backstage domain will not load, please wait and try again.
-### Bootstrap Proton
-
-> **NOTE:** The S3 location where deployment assets were uploaded to is in your AWS account, and should have a
-> name of the format `-cms-resources-`
-
-1. Sign in the [AWS Management Console](https://aws.amazon.com/console/), select your Region, and navigate to the
-[AWS Proton Service Templates](https://console.aws.amazon.com/proton/home/#/templates/services) page.
-2. Select `Create service template` for each module template you wish to register.
- ![Proton Create Service Template](documentation/images/readme/proton-create-service-template.png)
-3. Fill in the required fields (The following instructions detail how to register the CMS Vehicle Simulator Module
-template. The same steps can be applied to other modules as well by selecting the proper s3 path)
- 1. Select the `Use your own template bundle in S3 Option`
-
- ![Proton S3 bundle option](documentation/images/readme/proton-s3-bundle.png)
- 2. Select `Browse S3` and locate the bucket where the templates were uploaded.
-
- ![Proton Browse S3](documentation/images/readme/proton-browse-s3.png)
-
- ![Proton Choose Bucket](documentation/images/readme/proton-choose-bucket.png)
- 3. Locate the latest tar for the vehicle_simulator module template (the Amazon S3 path should be of the
- format `..//modules//proton/`) Press the `Choose` button
- ![Proton Choose Template Object](documentation/images/readme/proton-choose-template-tar.png)
- 4. In the repository, locate the [Vehicle Simulator Proton Template YAML](templates/modules/cms_vehicle_simulator_on_aws/template.yaml) and find the template name under the `metadataName` property. This will be under the `aws:proton:create-service` action. Use it to populate the `Service template name`
- ![Proton Find Template Name](documentation/images/readme/proton-module-template-name.png)
- ![Proton Enter Template Name](documentation/images/readme/proton-enter-template-name.png)
- 5. Set the `Compatible environment templates` to `cms_environment`
- ![Proton Template Compatible Env Setting](documentation/images/readme/proton-template-compatible-env-setting.png)
- 6. Leave the remaining settings as default and click `Create Service Template`.
- 7. After you receive a message stating `Successfully created service template cms_vehicle_simulator_on_aws.`, then select
- template version `1.0` and click `Publish` to make it available for use by Backstage
- ![Proton Publish Template Version](documentation/images/readme/proton-publish-template-version.png)
-
### Deploy CMS Modules via Backstage
#### CMS Module Deployment Order
-Some CMS on AWS modules have dependencies on other modules and must be deployed in order.
-Others do not have dependencies on other modules and can be deployed in any order, as long as the ACDP has been deployed first.
+All CMS modules have dependencies on the initial three deployments for configuring CMS.
+
+Some CMS on AWS modules have secondary dependencies on other modules and must be deployed in order.
+
+The rest of the modules do not have dependencies on other modules and can be deployed in any order after CMS Config.
The deployment order that must be observed is as follows:
+##### Deployment Order of Required CMS Config
+
+1. VPC
+2. Auth Setup
+3. CMS Config
+
##### Deployment Order of Modules with Dependencies
-1. CMS Authentication
-2. CMS Alerts
-3. CMS Connect and Store
+1. CMS Auth
+2. CMS Connect & Store
+3. CMS Alerts
4. CMS API
5. CMS EV Battery Health
+6. CMS FleetWise Connector
-##### Modules Without Dependencies
+##### Modules Without Dependencies After CMS Config
- CMS Vehicle Provisioning
- CMS Vehicle Simulator
#### Example Module Deployment via Backstage
-The following instructions detail how to deploy the CMS Vehicle Simulator Module.
+The following instructions detail how to deploy the CMS Vehicle Simulator module.
The same steps can be applied to other modules as well by replacing the URLs and names.
-1. Navigate to the CMS Backstage URL in a web browser (ROUTE53_BASE_DOMAIN that was specified during deployment).
-2. Sign in to Backstage using the credentials that were emailed to the user-email specified during deployment.
-3. Follow the prompts to create a new password and set up multi-factor authentication (MFA).
+1. Navigate to the Backstage URL in a web browser (ROUTE53_BASE_DOMAIN that was specified during deployment).
+2. Sign in to Backstage using the credentials that were emailed to the user-email specified during deployment.
+3. Follow the prompts to create a new password and set up multi-factor authentication (MFA).
4. On Backstage, navigate to the `Create` page available from the `Catalog` menu in the side bar.
- Select the `CHOOSE` button on the `CMS Vehicle Simulator on AWS` card.
+ Select the `CHOOSE` button on the `CMS Vehicle Simulator` card.
![Vehicle Simulator Choose Card](./documentation/images/readme/backstage-choose-vehicle-sim-card.png)
-5. Fill in the form as required by the Vehicle Simulator template and click the `Next Step` button.
- ![Vehicle Simulator Form Page](./documentation/images/readme/backstage-vehicle-simulator-form.png)
+5. Fill in the form as required by the Vehicle Simulator template and click the `Next` button and then the `Review` button.
+ ![Vehicle Simulator Form Page 1](./documentation/images/readme/backstage-vehicle-simulator-form-page-1.png)
+ ![Vehicle Simulator Form Page 2](./documentation/images/readme/backstage-vehicle-simulator-form-page-2.png)
6. Click the `Create` button.
@@ -464,9 +386,12 @@ The same steps can be applied to other modules as well by replacing the URLs and
![Vehicle Simulator Deployment Successful](./documentation/images/readme/backstage-vehicle-simulator-deployment-success.png)
-## Cost scaling
+## Cost Scaling
-Refer to the implementation guide for pricing information.
+In general, cost can differ dramatically based on usage of CMS.
+
+For at rest cost and detailed pricing information, see the
+[implementation guide](https://docs.aws.amazon.com/solutions/latest/connected-mobility-solution-on-aws/cost.html).
## Collection of Operational Metrics
@@ -475,125 +400,50 @@ the quality and features of the solution. For more information, including
how to disable this capability, please see the
[implementation guide](https://docs.aws.amazon.com/solutions/latest/connected-mobility-solution-on-aws/anonymized-data-collection.html).
-## Teardown
-
-This solution creates multiple CloudFormation deployments; both from the top level cdk deploy as well as additional stacks
-from Proton and CodePipeline executions. Some resources cannot be torn down directly via the AWS Console or by using the AWS CLI.
-
-The following commands assume the stage is `dev`, for other stages, replace `dev` with the appropriate value.
+## Uninstall the Solution
-1. Capture and store the deployment UUID of the solution.
+1. Capture and store the deployment UUIDs of the solution.
- This is used to look for any resources not destroyed by CloudFormation after teardown completes
```bash
- make get-deployment-uuid
+ make get-acdp-deployment-uuid
+ make get-cms-deployment-uuid
```
- output will be a uuidv4 string:
+ the outputs will be uuidv4 strings, capture and store both:
```bash
XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
```
-2. Delete CMS on AWS Modules in AWS Proton and CloudFormation that were deployed via Backstage
- 1. In AWS Proton, Navigate to the `Services` view and delete any service attached to the
- `cms-environment` Environment. Wait until all services successfully delete.
-
- > **NOTE:** You have to click the link into the service to be able to delete it via the `Actions` dropdown
-
- ![Delete CMS Module Proton Service](documentation/images/readme/delete-cms-module-proton-service.png)
- - If the delete fails in AWS Proton, and the CodeBuild `cdk destroy` task shows an error in the CodeBuild logs, most likely the module attempting
- to be deleted has a dependency blocking the deletion. Continue tearing down the rest of the modules and try again.
- Refer to the AWS Proton CodeBuild logs and CloudFormation console output for additional information.
- - If the delete fails in AWS Proton, but the CloudFormation stack for the module is deleted successfully,
- most likely AWS Proton needs an additional role for account level CodeBuild. You might also see an error on the top of the screen reading
- `Validation exception...Operation cannot be run until pipeline roles have been configured.`
- or `AccountSettings.pipelineCodeBuildRoleArn has not been configured.`.
- In this case, go to `Account Settings` and configure a role.
- > **WARNING:** Proton's UI requires a GitHub Repository connection to configure roles. To get around this, set the roles using a CLI command.
- Note that the CLI command uses the arn value for the roles in your account. Replace the "1" and "X" placeholders in the following command with
- the values found in the IAM console for your account.
-
- ```bash
- aws proton update-account-settings \
- --pipeline-codebuild-role-arn arn:aws:iam::11111111111:role/cms-dev-cmsprotonenvironmentprotoncodebuildroleXXX-XXXXXXXXXXXX \
- --pipeline-service-role-arn arn:aws:iam::11111111111:role/service-role/proton_role
- ```
-
- 2. After AWS Proton shows that all services have been deleted, verify in the
- CloudFormation console that all CMS on AWS modules have been deleted, and if not, delete them.
-
-3. Delete AWS Proton Service Templates, Environment, and Environment Templates.
- > **NOTE:** if you wish to keep services that have been deployed via backstage, skip these steps.
- 1. Navigate to the `Service Templates` view in AWS Proton and delete any CMS on AWS service templates.
-
- ![Delete Proton Service Templates](documentation/images/readme/delete-proton-service-templates.png)
-
- > **WARNING:** If you receive the following Validation Exception: `Service template has major versions that must first be deleted.`,
- then you must run the delete command multiple times until all of the major versions have been deleted
- for the service template.
-
- 2. Navigate to the `Environments` view and delete the `cms_environment` Environment.
-
- ![Delete Proton Environment](documentation/images/readme/delete-proton-environment.png)
-
- > **WARNING:** If you receive the following Validation Exception:
- `Environment template has major versions that must first be deleted.`, then you must run the
- delete command multiple times until all of the major versions have been deleted for
- the environment template.
-
- 3. Navigate to the `Environment Templates` view and delete the `cms_environment` Environment Template.
- ![Delete Proton Environment Template](documentation/images/readme/delete-proton-env-template.png)
-
- 4. Validate on the AWS Proton dashboard that all resources have been removed.
- ![Validate Proton Teardown](documentation/images/readme/validate-proton-teardown.png)
+1. Delete CMS modules in order.
- 5. Navigate to CloudFormation and delete the AWS Proton CodeBuild stack (AWSProton-Codebuild-#######).
- ![Delete Proton CodeBuild CFN](documentation/images/readme/delete-proton-codebuild-cfn.png)
-
- 6. Verify that the `cms-environment` stack was removed when tearing down AWS Proton, and if not, delete it.
-
-4. Delete the Backstage CloudFormation Stacks
-
- Navigate to CloudFormation and delete the following stacks:
- - cms-backstage-dev
- - cms-backstage-env-dev
-
- ![Delete Backstage Stacks](documentation/images/readme/delete-backstage-cfn.png)
+ ```bash
+ make destroy
+ ```
- > **NOTE:** The `cms-backstage-dev` stack might fail to delete due to the ACM certificate creation custom resource.
+ > **NOTE:** Backstage might fail to delete due to the ACM certificate creation custom resource.
After delete fails, click delete again and select retain on the custom resource.
This will not leave any resources in the account.
![Delete Backstage with Cert Error](documentation/images/readme/delete-backstage-on-cert-error.png)
-1. Delete the CMS Backstage Amazon ECR repository
-
- Navigate to Amazon ECR, and delete the repository called `backstage`.
-
- ![Delete ECR Repository](documentation/images/readme/ecr-delete-backstage-repo.png)
-
-2. Delete the CMS on AWS CloudFormation Stacks
-
- > **NOTE:** The `cms-dev` stack in this step can only be deleted if the prevous steps for deleting `cms-backstage-*` stacks have finished.
- Please wait for the deletes to finish in the CloudFormation console before moving on.
+1. Delete the Backstage ACM Certificate (optional)
- Navigate to CloudFormation and delete the `cms-environment` and `cms-dev` stacks.
+ Navigate to Amazon Certificate Manager, and delete the Backstage certificate.
- > **WARNING:** This is your last opportunity to capture the deployment UUID. Please make sure you have captured
- it using the make command specified in step 1 of the [Teardown](#teardown) section.
+1. Manually cleanup the following resources:
- ![Delete CMS Dev Stack](documentation/images/readme/cfn-delete-cms-dev-stack.png)
+- S3 buckets
+- DynamoDB tables
+- Cognito user pool
+- KMS keys
-3. Manually cleanup the following resources:
- - S3 Buckets
- - Cognito User Pool
- - KMS Keys
+ Locate the leftover resources using the following command which first requires you to export the `DEPLOYMENT_UUID`
+ variable using each of the values previously acquired from AWS Systems Manager.
- Locate the leftover resources using the following command which first requires you to export the `DEPLOYMENT_UUID` variable using the value previously acquired from AWS Systems Manager.
-
- If you tore down the CMS on AWS stack without capturing the UUID, the below command can be run by removing
+ If you tore down the ACDP stack without capturing the UUIDs, the below command can be run by removing
the `Solutions:DeploymentUUID` Key filter, however the results will include other CMS on AWS stacks if they exist,
so use this method with caution.
@@ -606,86 +456,60 @@ The following commands assume the stage is `dev`, for other stages, replace `dev
--query "ResourceTagMappingList[*].ResourceARN"
```
- This query results in a list of ARNs to assist you with locating the resources in the AWS Console. Resources can then be
- manually deleted, or deleted via a script, utilizing the resource ARNs where appropriate.
-
- > **WARNING:** Some resources may take some time to cleanup after CloudFormation finishes tearing down, and could show in the
- output even if they no longer exist. For example, Amazon VPC, Fargate, and Amazon ECS resources can remain queryable for up to
- 30 minutes after deletion.
-
- Example Output:
- ```json
- [
- "arn:aws:cognito-idp:us-east-1:11111111111:userpool/us-east-1_XXXXXXXX",
- "arn:aws:dynamodb:us-east-1:11111111111:table/cms-alerts-on-aws-stack-dev-cmsalertsusersubscriptionsconstructuseremailsubscriptionstableXXXXXXXXXXX",
-
- "arn:aws:ecs:us-east-1:11111111111:task-definition/cms-backstage-dev:1",
- "arn:aws:ecs:us-east-1:11111111111:task-definition/cms-backstage-dev:2",
- "arn:aws:s3:::cms-connect-store-on-aws-connectstoreconnectstore-XXXXXXXXXXX",
- "arn:aws:s3:::cms-dev-cmsprotonenvironmentprotonenvironmentbuck-XXXXXXXXXXX",
- "arn:aws:dynamodb:us-east-1:11111111111:table/cms-alerts-on-aws-stack-dev-cmsalertsnotificationconstructnotificationstableXXXXXXX-XXXXXXXXXX",
- "arn:aws:logs:us-east-1:11111111111:log-group:cms-backstage-dev-cmsbackstagebackstageloggroupXXXXXXXX-XXXXX",
- "arn:aws:logs:us-east-1:11111111111:log-group:cms-dev-cmspipelinescmsvpcloggroupXXXXXXXX-XXXXX",
- "arn:aws:s3:::cms-dev-cmspipelinesbackstagecodepipelineartifact-XXXXXXXXXX",
- "arn:aws:s3:::cms-backstage-dev-cmsbackstagebackstageelblogsbuc-XXXXXXXXXX",
- "arn:aws:acm:us-east-1:11111111111:certificate/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
- "arn:aws:cognito-idp:us-east-1:11111111111:userpool/us-east-1_XXXXXXX",
- "arn:aws:logs:us-east-1:11111111111:log-group:cms-connect-store-on-aws-stack-dev-connectstoreconnectstoreiotconnectivitylog1234abc-XXXXXXXXX",
- "arn:aws:rds:us-east-1:11111111111:cluster-snapshot:cms-backstage-env-dev-snapshot-cmsbackstageenvbackstageaurorapostgresXXXXX-XXXXXXXXX",
- "arn:aws:s3:::cms-connect-store-on-aws-connectstoreconnectstore-XXXXXXXXXXX",
- "arn:aws:s3:::cms-backstage-env-dev-cmsbackstageenvbackstagecat-XXXXXXXXXXX",
- "arn:aws:kms:us-east-1:11111111111:key/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
- "arn:aws:kms:us-east-1:11111111111:key/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
- "arn:aws:kms:us-east-1:11111111111:key/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
- "arn:aws:kms:us-east-1:11111111111:key/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
- ]
- ```
+ This query results in a list of ARNs to assist you with locating the resources in the AWS Management Console.
+ Resources can then be manually deleted, or deleted via a script, utilizing the resource ARNs where appropriate.
+
+ > **WARNING:** Some resources may take some time to cleanup after CloudFormation finishes tearing down, and could
+ show in the output even if they no longer exist. For example, Amazon VPC, Fargate, and Amazon ECS resources can
+ remain queryable for up to 30 minutes after deletion.
## Developer Guide
### Logging
+
By default, this solution implements safe logging which does not expose any sensitive or vulnerable information.
CMS on AWS does not currently support a one-step system for enabling more detailed debug logs.
To add additional logs to the solution, you are required to alter the source code.
Examples of logging implementations can be found in the existing Lambda functions.
-#### Lambda functions
+#### Lambda Functions
+
By default, the solution disabled Lambda event logging, which contains sensitive information.
However, this functionality is provided by the AWS Lambda Powertools library which is utilized by each Lambda function.
-To quickly enable event logging, navigate to the Lambda function in the AWS Console and add the following Lambda environment variable:
+To quickly enable event logging, navigate to the Lambda function in the AWS Management Console and add the following Lambda
+environment variable:
-```
+```bash
POWERTOOLS_LOGGER_LOG_EVENT="true"
```
For other logging options and methods for enabling event logging,
see the [AWS Lambda Powertools documentation](https://docs.powertools.aws.dev/lambda/python/latest/core/logger/).
-#### Backstage logs
+#### Backstage Logs
+
By default, the solution's deployment instructions deploy the ACDP and Backstage with a log level of "INFO".
To enable debug logs for Backstage, change the following environment variable when you deploy the solution:
-```
-BACKSTAGE_LOG_LEVEL="DEBUG"
+```bash
+export BACKSTAGE_LOG_LEVEL="debug"
```
### Pre-Commit Hooks
-This solution contains a number of linters and checks to ensure code quality.
-If you are not planning to commit code back to source, you can run the pre-commit hooks manually
-using the following command:
+This solution contains a number of linters and checks to ensure code quality. If you are not planning to commit code
+back to source, you can run the pre-commit hooks manually using the following command:
```bash
-pre-commit run --all
+make pre-commit-all
```
### Unit Test
-After making changes, run unit tests to make sure added customization
-passes the tests:
+After making changes, run unit tests to make sure added customization passes the tests:
```bash
-./deployment/run-unit-tests.sh
+make unit-test
```
## License
@@ -694,7 +518,7 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License").
You may not use this file except in compliance with the License.
-You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+You may obtain a copy of the License at
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/cdk.json b/cdk.json
deleted file mode 100644
index 9e10395c..00000000
--- a/cdk.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "app": "python3 -m source.infrastructure.app",
- "watch": {
- "include": [
- "**"
- ],
- "exclude": [
- "README.md",
- "cdk*.json",
- "requirements*.txt",
- "source.bat",
- "**/__init__.py",
- "python/__pycache__",
- "tests"
- ]
- },
- "context": {
- "dep-layer-name": "cms_dependency_layer",
- "app-location": "source/infrastructure",
- "nag-enforce": false
- }
-}
diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh
index e262119c..05345cf4 100755
--- a/deployment/build-s3-dist.sh
+++ b/deployment/build-s3-dist.sh
@@ -1,220 +1,10 @@
#!/bin/bash
-#
-# This script will perform the following tasks:
-# 1. Remove any old dist files from previous runs.
-# 2. Install dependencies for the cdk-solution-helper; responsible for
-# converting standard 'cdk synth' output into solution assets.
-# 3. Build and synthesize your CDK project.
-# 4. Run the cdk-solution-helper on template outputs and organize
-# those outputs into the /global-s3-assets folder.
-# 5. Organize source code artifacts into the /regional-s3-assets folder.
-# 6. Remove any temporary files used for staging.
-#
-# This script should be run from the repo's root directory
-# ./deployment/build-s3-dist.sh dist-bucket-name template-bucket-name solution-name version-code
-#
-# Parameters:
-# - dist-bucket-name: Name for the S3 bucket location where the assets (dependency layers, lambda handlers etc)
-# will be expected to be uploaded to be able to deploy the template
-# - solution-name: trademarked name of the solution
-# - version-code: version of the solution
-# - template-bucket-name: Name for the S3 bucket location where the assets (stacks, nested stacks)
-# will be expected to be uploaded to be able to deploy the template
-#
-# For example: ./deployment/build-s3-dist.sh solutions-features my-solution v1.0.0 solutions-features-reference
-# The template will then expect the source code to be located in the solutions-features-[region_name] bucket
-# The template will then expect the stacks and nested stacks to be located in the solutions-features-reference bucket
-#
-# The primary stack template stored in the /global-s3-assets directory should be deployable
-# through the cloudformation console once the contents of the /global-s3-assets are uploaded
-# to the s3 bucket named template-bucket-name and the contents of the /regional-s3-assets
-# directory are uploaded to the s3 bucket named dist-bucket-name.
+# This file is not used, but it is required by the pipeline checks. Specifically viperlight pubcheck.
+# This can be replaced with `touch ./deployment/build-s3-dist.sh` in the buildspec.yaml which also
+# gets picked up on the check that makes sure that build-s3-dist.sh is "called" in the buildspec.
+# It doesn't actually check that it is called, just does a basic grep.
-# Activate local environment
-echo "===================================================="
-echo "Activating venv found in $PWD"
-echo "===================================================="
-source ./.venv/bin/activate
-
-[ "$DEBUG" == 'true' ] && set -x
-set -e
-
-# set run_module_scripts="yes" if all the build-s3-dist scripts for the modules need to be run
-run_module_scripts="yes"
-
-dist_bucket_name="$1"
-template_bucket_name="$2"
-solution_name="$3"
-solution_version="$4"
-
-# Check to see if input has been provided:
-if [ -z "$dist_bucket_name" ] || [ -z "$template_bucket_name" ] || [ -z "$solution_name" ] || [ -z "$solution_version" ]; then
- read -p "Distribution Bucket Name [connected-mobility-solution-on-aws]: " dist_bucket_name
- dist_bucket_name=${dist_bucket_name:-"connected-mobility-solution-on-aws"}
- read -p "Template Bucket Name [connected-mobility-solution-on-aws]: " template_bucket_name
- template_bucket_name=${template_bucket_name:-"connected-mobility-solution-on-aws"}
- read -p "Solution Name [connected-mobility-solution-on-aws]: " solution_name
- solution_name=${solution_name:-"connected-mobility-solution-on-aws"}
- read -p "Solution Version [v1.0.4]: " solution_version
- solution_version=${solution_version:-"v1.0.4"}
-fi
-
-dashed_version="${solution_version//./$'_'}"
-
-# Get reference for all important folders
-project_dir="$PWD"
-deployment_dir="$PWD/deployment"
-staging_dist_dir="$deployment_dir/staging"
-template_dist_dir="$deployment_dir/global-s3-assets"
-build_dist_dir="$deployment_dir/regional-s3-assets"
-cdk_source_dir="$deployment_dir/../"
-
-echo "------------------------------------------------------------------------------"
-echo "[Init] Remove any old dist files from previous runs"
-echo "------------------------------------------------------------------------------"
-rm -rf $template_dist_dir
-mkdir -p $template_dist_dir
-
-rm -rf $build_dist_dir
-mkdir -p $build_dist_dir
-
-rm -rf $staging_dist_dir
-mkdir -p $staging_dist_dir
-
-echo "------------------------------------------------------------------------------"
-echo "[Init] Install dependencies for cdk-solution-helper"
-echo "------------------------------------------------------------------------------"
-cd $deployment_dir/cdk-solution-helper
-npm install
-npm ci --omit=dev
-
-echo "------------------------------------------------------------------------------"
-echo "[Build] Build project specific assets"
-echo "------------------------------------------------------------------------------"
-
-echo "------------------------------------------------------------------------------"
-echo "[Install] Installing CDK"
-echo "------------------------------------------------------------------------------"
-
-npm install -g aws-cdk
-echo "cdk version: $(cdk version)"
-## Option to suppress the Override Warning messages while synthesizing using CDK
-export overrideWarningsEnabled=false
-echo "setting override warning to $overrideWarningsEnabled"
-
-echo "------------------------------------------------------------------------------"
-echo "[Synth] Synthesize Stack"
-echo "------------------------------------------------------------------------------"
-
-cd $cdk_source_dir
-make synth-staging
-
-cd $staging_dist_dir
-rm tree.json manifest.json cdk.out
-
-echo "------------------------------------------------------------------------------"
-echo "[Packing] Template artifacts"
-echo "------------------------------------------------------------------------------"
-cp $staging_dist_dir/*.template.json $template_dist_dir/
-rm *.template.json
-
-for f in $template_dist_dir/*.template.json; do
- mv -- "$f" "${f%.template.json}.template";
-done
-
-node $deployment_dir/cdk-solution-helper/index
-
-echo "------------------------------------------------------------------------------"
-echo "Updating placeholders"
-echo "------------------------------------------------------------------------------"
-sedi=(-i)
-if [[ "$OSTYPE" == "darwin"* ]]; then
- sedi=(-i "")
-fi
-
-for file in $template_dist_dir/*.template
-do
- replace="s/%%DIST_BUCKET_NAME%%/$dist_bucket_name/g"
- sed "${sedi[@]}" -e $replace $file
-
- replace="s/%%SOLUTION_NAME%%/$solution_name/g"
- sed "${sedi[@]}" -e $replace $file
-
- replace="s/%%VERSION%%/$solution_version/g"
- sed "${sedi[@]}" -e $replace $file
-
- replace="s/%%TEMPLATE_BUCKET_NAME%%/$template_bucket_name/g"
- sed "${sedi[@]}" -e $replace $file
-
- replace="s/%%DASHED_VERSION%%/$dashed_version/g"
- sed "${sedi[@]}" -e $replace $file
-
- # replace cdk-xxxxxxx-assets-* bucket with the assets bucket name
- replace="s/cdk-[a-z0-9]+-assets-\\$\{AWS::AccountId\}/$dist_bucket_name/g"
- sed "${sedi[@]}" -E $replace $file
-
- replace="s/cdk-[a-z0-9]+-assets-/$dist_bucket_name/g"
- sed "${sedi[@]}" -E $replace $file
-done
-
-echo "------------------------------------------------------------------------------"
-echo "[Packing] Source code artifacts"
-echo "------------------------------------------------------------------------------"
-# ... For each asset.*.zip source code artifact in the temporary /staging folder...
-cd $staging_dist_dir
-for f in `find . -name "*.zip" -mindepth 1 -maxdepth 1 -type f`; do
- # Rename the artifact, removing the period for handler compatibility
- pfname="$(basename -- $f)"
- fname="$(echo $pfname | sed -e 's/asset\./asset/g')"
- mv $f $fname
-
- # Copy the artifact from /staging to /regional-s3-assets
- cp $fname $build_dist_dir
-done
-
-for d in `find . -mindepth 1 -maxdepth 1 -type d`; do
- # Rename the artifact, removing the period for handler compatibility
- pfname="$(basename -- $d)"
- fname="$(echo $pfname | sed -e 's/\.//g')"
- mv $d $fname
-
- # Zip artifacts from asset folder
- cd $fname
- zip -r ../$fname.zip * > /dev/null
- cd ..
-
- # Copy the zipped artifact from /staging to /regional-s3-assets
- cp $fname.zip $build_dist_dir
-
- # Remove the old artifacts from /staging
- rm -rf $fname
- rm $fname.zip
-done
-
-echo "------------------------------------------------------------------------------"
-echo "[Cleanup] Remove temporary files"
-echo "------------------------------------------------------------------------------"
-cd $deployment_dir
-rm -rf $staging_dist_dir
-
-echo "------------------------------------------------------------------------------"
-echo "[Info] Deployment Assets Created"
-echo "------------------------------------------------------------------------------"
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-NC='\033[0m' # No Color
-
-echo -e "${YELLOW}If you have not previously created S3 buckets to upload assets to, then run: ${NC}"
-echo -e "${GREEN}aws s3 mb s3://$template_bucket_name ${NC}"
-echo -e "${GREEN}aws s3 mb s3://$dist_bucket_name ${NC}"
-
-echo -e "${YELLOW}To upload the assets, run: ${NC}"
-echo -e "${GREEN}aws s3 cp $template_dist_dir s3://$template_bucket_name/$solution_name/$solution_version/ --recursive ${NC}"
-echo -e "${GREEN}aws s3 cp $build_dist_dir s3://$dist_bucket_name/$solution_name/$solution_version/ --recursive ${NC}"
-
-# Run build-s3-dist scripts of all modules
-if [[ $run_module_scripts == "yes" ]]; then
- cd $project_dir
- $project_dir/deployment/run-module-scripts.sh $(basename $0) $dist_bucket_name $template_bucket_name $solution_name $solution_version
-fi
+# If you, the reader, really would like this file to do something, uncomment the below line.
+# make -C ../Makefile build
+# You should note how redundant it was to uncomment that line.
diff --git a/deployment/cdk-solution-helper/README.md b/deployment/cdk-solution-helper/README.md
deleted file mode 100644
index 8554eb44..00000000
--- a/deployment/cdk-solution-helper/README.md
+++ /dev/null
@@ -1,152 +0,0 @@
-# cdk-solution-helper
-
-A lightweight helper function that cleans-up synthesized templates from the AWS Cloud Development Kit (CDK) and prepares
-them for use with the AWS Solutions publishing pipeline. This function performs the following tasks:
-
-#### Lambda function preparation
-
-Replaces the AssetParameter-style properties that identify source code for Lambda functions with the common variables
-used by the AWS Solutions publishing pipeline.
-
-- `Code.S3Bucket` is assigned the `%%DIST_BUCKET_NAME%%` placeholder value.
-- `Code.S3Key` is assigned the `%%SOLUTION_NAME%%`/`%%VERSION%%` placeholder value.
-- `Handler` is given a prefix identical to the artifact hash, enabling the Lambda function to properly find the handler in the extracted source code package.
-
-These placeholders are then replaced with the appropriate values using the default find/replace operation run by the pipeline.
-
-Before:
-```
-"examplefunction67F55935": {
- "Type": "AWS::Lambda::Function",
- "Properties": {
- "Code": {
- "S3Bucket": {
- "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3Bucket54E71A95"
- },
- "S3Key": {
- "Fn::Join": [
- "",
- [
- {
- "Fn::Select": [
- 0,
- {
- "Fn::Split": [
- "||",
- {
- "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1"
- }
- ]
- }
- ]
- },
- {
- "Fn::Select": [
- 1,
- {
- "Fn::Split": [
- "||",
- {
- "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1"
- }
- ]
- }
- ]
- }
- ]
- ]
- }
- }, ...
- Handler: "index.handler", ...
-```
-
-After helper function run:
-```
-"examplefunction67F55935": {
- "Type": "AWS::Lambda::Function",
- "Properties": {
- "Code": {
- "S3Bucket": "%%DIST_BUCKET_NAME%%",
- "S3Key": "%%SOLUTION_NAME%%/%%VERSION%%/assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip"
- }, ...
- "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler"
-```
-
-After build script run:
-```
-"examplefunction67F55935": {
- "Type": "AWS::Lambda::Function",
- "Properties": {
- "Code": {
- "S3Bucket": "solutions",
- "S3Key": "trademarked-solution-name/v1.0.4/asset.d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip"
- }, ...
- "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler"
-```
-
-After CloudFormation deployment:
-```
-"examplefunction67F55935": {
- "Type": "AWS::Lambda::Function",
- "Properties": {
- "Code": {
- "S3Bucket": "solutions-us-east-1",
- "S3Key": "trademarked-solution-name/v1.0.4/asset.d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip"
- }, ...
- "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler"
-```
-
-#### Template cleanup
-
-Cleans-up the parameters section and improves readability by removing the AssetParameter-style fields that would have
-been used to specify Lambda source code properties. This allows solution-specific parameters to be highlighted and
-removes unnecessary clutter.
-
-Before:
-```
-"Parameters": {
- "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3Bucket54E71A95": {
- "Type": "String",
- "Description": "S3 bucket for asset \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\""
- },
- "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1": {
- "Type": "String",
- "Description": "S3 key for asset version \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\""
- },
- "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7ArtifactHash7AA751FE": {
- "Type": "String",
- "Description": "Artifact hash for asset \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\""
- },
- "CorsEnabled" : {
- "Description" : "Would you like to enable Cross-Origin Resource Sharing (CORS) for the image handler API? Select 'Yes' if so.",
- "Default" : "No",
- "Type" : "String",
- "AllowedValues" : [ "Yes", "No" ]
- },
- "CorsOrigin" : {
- "Description" : "If you selected 'Yes' above, please specify an origin value here. A wildcard (*) value will support any origin.",
- "Default" : "*",
- "Type" : "String"
- }
- }
- ```
-
-After:
-```
-"Parameters": {
- "CorsEnabled" : {
- "Description" : "Would you like to enable Cross-Origin Resource Sharing (CORS) for the image handler API? Select 'Yes' if so.",
- "Default" : "No",
- "Type" : "String",
- "AllowedValues" : [ "Yes", "No" ]
- },
- "CorsOrigin" : {
- "Description" : "If you selected 'Yes' above, please specify an origin value here. A wildcard (*) value will support any origin.",
- "Default" : "*",
- "Type" : "String"
- }
- }
- ```
-
-***
-© Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
diff --git a/deployment/cdk-solution-helper/index.js b/deployment/cdk-solution-helper/index.js
deleted file mode 100644
index 7fa5b667..00000000
--- a/deployment/cdk-solution-helper/index.js
+++ /dev/null
@@ -1,216 +0,0 @@
-/**
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
- * with the License. A copy of the License is located at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
- * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
- * and limitations under the License.
- */
-
-// Imports
-const fs = require("fs");
-
-// Paths
-const global_s3_assets = "../global-s3-assets";
-
-function substituteLambdaAssets(template, resources) {
- // Clean-up Lambda function code dependencies
- const lambdaFunctions = Object.keys(resources).filter(function (key) {
- return resources[key].Type === "AWS::Lambda::Function";
- });
- lambdaFunctions.forEach(function (f) {
- const fn = template.Resources[f];
- let prop;
- if (fn.Properties.hasOwnProperty("Code")) {
- prop = fn.Properties.Code;
- } else if (fn.Properties.hasOwnProperty("Content")) {
- prop = fn.Properties.Content;
- }
-
- if (prop.hasOwnProperty("S3Bucket")) {
- // Set the S3 key reference
- let artifactHash = Object.assign(prop.S3Key);
- const assetPath = `asset${artifactHash}`;
- prop.S3Key = `%%SOLUTION_NAME%%/%%VERSION%%/${assetPath}`;
-
- // Set the S3 bucket reference
- prop.S3Bucket = {
- "Fn::Sub": "%%DIST_BUCKET_NAME%%-${AWS::Region}",
- };
- } else {
- console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
- }
- });
-}
-
-function substituteLambdaLayerAssets(template, resources) {
- const lambdaLayers = Object.keys(resources).filter(function (key) {
- return resources[key].Type === "AWS::Lambda::LayerVersion";
- });
- lambdaLayers.forEach(function (f) {
- const fn = template.Resources[f];
- let prop;
- if (fn.Properties.hasOwnProperty("Content")) {
- prop = fn.Properties.Content;
- }
-
- if (prop.hasOwnProperty("S3Bucket")) {
- // Set the S3 key reference
- let artifactHash = Object.assign(prop.S3Key);
- const assetPath = `asset${artifactHash}`;
- prop.S3Key = `%%SOLUTION_NAME%%/%%VERSION%%/${assetPath}`;
-
- // Set the S3 bucket reference
- prop.S3Bucket = {
- "Fn::Sub": "%%DIST_BUCKET_NAME%%-${AWS::Region}",
- };
- } else {
- console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
- }
- });
-}
-
-function substituteServerlessFunctionAssets(template, resources) {
- const serverlessFunctions = Object.keys(resources).filter(function (key) {
- return resources[key].Type === "AWS::Serverless::Function";
- });
- serverlessFunctions.forEach(function (f) {
- const fn = template.Resources[f];
- let prop;
- if (fn.Properties.hasOwnProperty("CodeUri")) {
- prop = fn.Properties.CodeUri;
- }
-
- if (prop.hasOwnProperty("Bucket")) {
- // Set the S3 key reference
- let artifactHash = Object.assign(prop.Key);
- const assetPath = `asset${artifactHash}`;
- prop.Key = `%%SOLUTION_NAME%%/%%VERSION%%/${assetPath}`;
-
- // Set the S3 bucket reference
- prop.Bucket = {
- "Fn::Sub": "%%DIST_BUCKET_NAME%%-${AWS::Region}",
- };
- } else {
- console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
- }
- });
-}
-
-function substituteCDKBucketDeploymentAssets(template, resources) {
- const cdkBucketDeployments = Object.keys(resources).filter(function (key) {
- return resources[key].Type === "Custom::CDKBucketDeployment";
- });
- cdkBucketDeployments.forEach(function (f) {
- const fn = template.Resources[f];
- let prop = fn.Properties;
-
- if (prop.hasOwnProperty("SourceBucketNames")) {
- // Set the S3 key reference
- let artifactHash = Object.assign(prop.SourceObjectKeys);
- const assetPath = `asset${artifactHash}`;
- prop.SourceObjectKeys = [`%%SOLUTION_NAME%%/%%VERSION%%/${assetPath}`];
-
- // Set the S3 bucket reference
- prop.SourceBucketNames = [
- {
- "Fn::Sub": "%%DIST_BUCKET_NAME%%-${AWS::Region}",
- },
- ];
- } else {
- console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
- }
- });
-}
-
-function substituteCodeCommitRepoAssets(template, resources) {
- const codeCommitRepos = Object.keys(resources).filter(function (key) {
- return resources[key].Type === "AWS::CodeCommit::Repository";
- });
- codeCommitRepos.forEach(function (f) {
- const fn = template.Resources[f];
- let prop;
-
- if (fn.Properties.hasOwnProperty("Code")) {
- prop = fn.Properties.Code;
- }
-
- if (prop.hasOwnProperty("S3")) {
- prop = prop.S3;
- // Set the S3 key reference
- let artifactHash = Object.assign(prop.Key);
- const assetPath = `asset${artifactHash}`;
- prop.Key = `%%SOLUTION_NAME%%/%%VERSION%%/${assetPath}`;
- // Set the S3 bucket reference
- prop.Bucket = {
- "Fn::Sub": "%%DIST_BUCKET_NAME%%-${AWS::Region}",
- };
- } else {
- console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
- }
- });
-}
-
-function substituteNestedStackAssets(template, resources) {
- // Clean-up nested template stack dependencies
- const nestedStacks = Object.keys(resources).filter(function (key) {
- return resources[key].Type === "AWS::CloudFormation::Stack";
- });
-
- nestedStacks.forEach(function (f) {
- const fn = template.Resources[f];
- let assetPath = fn.Metadata["aws:asset:path"];
- // get the base name of the asset path file. Trim the .json at the end
- if (
- assetPath.substring(assetPath.length - 5, assetPath.length) === ".json"
- ) {
- assetPath = assetPath.substring(0, assetPath.length - 5);
- }
-
- fn.Properties.TemplateURL = {
- "Fn::Join": [
- "",
- [
- "https://%%TEMPLATE_BUCKET_NAME%%.s3.",
- {
- Ref: "AWS::URLSuffix",
- },
- "/",
- `%%SOLUTION_NAME%%/%%VERSION%%/${assetPath}`,
- ],
- ],
- };
- });
-}
-
-// For each template in global_s3_assets ...
-fs.readdirSync(global_s3_assets).forEach((file) => {
- // Import and parse template file
- const raw_template = fs.readFileSync(`${global_s3_assets}/${file}`);
- let template = JSON.parse(raw_template);
- const resources = template.Resources ? template.Resources : {};
-
- substituteLambdaAssets(template, resources);
- substituteLambdaLayerAssets(template, resources);
- substituteServerlessFunctionAssets(template, resources);
- substituteCDKBucketDeploymentAssets(template, resources);
- substituteCodeCommitRepoAssets(template, resources);
- substituteNestedStackAssets(template, resources);
-
- // Clean-up parameters section
- const parameters = template.Parameters ? template.Parameters : {};
- const assetParameters = Object.keys(parameters).filter(function (key) {
- return key.includes("AssetParameters");
- });
- assetParameters.forEach(function (a) {
- template.Parameters[a] = undefined;
- });
-
- // Output modified template file
- const output_template = JSON.stringify(template, null, 2);
- fs.writeFileSync(`${global_s3_assets}/${file}`, output_template);
-});
diff --git a/deployment/clean-for-deploy.sh b/deployment/clean-for-deploy.sh
deleted file mode 100755
index 9b227b71..00000000
--- a/deployment/clean-for-deploy.sh
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/bin/bash
-
-showHelp() {
-# `cat << EOF` This means that cat should stop reading when EOF is detected
-cat << EOF
-Usage: ./deployment/clean-for-deploy.sh --help
-
-Clean unwanted files when deploying this project.
-
--h, --help Display help
-
--r, --release-build Remove the release build files
-
--d, --dependencies Remove the dependencies and virtual environments
-
--a, --all Remove all artifacts
-
-EOF
-# EOF is found above and hence cat command stops reading. This is equivalent to echo but much neater when printing out.
-}
-
-release_build=""
-dependencies=""
-while [[ $# -gt 0 ]]
-do
-key="$1"
-case $key in
- -h|--help)
- showHelp
- exit 0
- ;;
- -r|--release-build)
- release_build="yes"
- shift
- ;;
- -d|--dependencies)
- dependencies="yes"
- shift
- ;;
- -a|--all)
- release_build="yes"
- dependencies="yes"
- shift
- ;;
- *)
- shift
-esac
-done
-
-echo "------------------------------------------------------------------------------"
-echo "[Delete] Clean up Javascript files"
-echo "------------------------------------------------------------------------------"
-
-# MEDIUM: find javascript install and build directories
-find . -name "dist" -type d -prune -exec rm -rf '{}' +
-find . -name "dist-types" -type d -prune -exec rm -rf '{}' +
-find . -name "build" -type d -not -path "**/.venv/*" -prune -exec rm -rf '{}' +
-
-if [[ $dependencies == "yes" ]]; then
- # MEDIUM: find javascript install and build directories
- find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
-
- # SMALL: find javascript lock files, these are hovering around 1-2MB
- find . -name "package-lock.json" -type f -prune -exec rm -rf '{}' +
- find . -name "yarn.lock" -type f -prune -exec rm -rf '{}' +
-fi
-
-echo "------------------------------------------------------------------------------"
-echo "[Delete] Clean up CDK files"
-echo "------------------------------------------------------------------------------"
-
-# LARGE: find cdk build directories
-find . -name "cdk.out" -type d -prune -exec rm -rf '{}' +
-find . -name "generated_models" -type d -prune -exec rm -rf '{}' +
-
-echo "------------------------------------------------------------------------------"
-echo "[Delete] Clean up Python files"
-echo "------------------------------------------------------------------------------"
-
-if [[ $dependencies == "yes" ]]; then
- # MEDIUM: find any child virtual environments
- find . -mindepth 2 -name ".venv" -type d -prune -exec rm -rf '{}' +
- find . -name "Pipfile.lock" -type f -prune -exec rm -rf '{}' +
-fi
-
-# SMALL: find python noise
-find . -name "__pycache__" -type d -prune -exec rm -rf '{}' +
-find . -name ".pytest_cache" -type d -prune -exec rm -rf '{}' +
-
-echo "------------------------------------------------------------------------------"
-echo "[Delete] Clean up Lambda Layer files"
-echo "------------------------------------------------------------------------------"
-
-# MEDIUM: find layers
-find . -name "None" -type d -prune -exec rm -rf '{}' +
-find . -name "*_dependency_layer" -type d -prune -exec rm -rf '{}' +
-find . -name "*_dep_layer" -type d -prune -exec rm -rf '{}' +
-find . -name "*-dep-layer" -type d -prune -exec rm -rf '{}' +
-
-echo "------------------------------------------------------------------------------"
-echo "[Delete] Clean up AWS Chalice files"
-echo "------------------------------------------------------------------------------"
-
-# MEDIUM: find chalice
-find . -name "chalice.out" -type d -prune -exec rm -rf '{}' +
-find . -name "deployments" -type d -prune -exec rm -rf '{}' +
-
-echo "------------------------------------------------------------------------------"
-echo "[Delete] Clean up Proton tar files"
-echo "------------------------------------------------------------------------------"
-
-# MEDIUM: find environment tars
-find . -name "environment_tars" -type d -prune -exec rm -rf '{}' +
-
-if [[ $release_build == "yes" ]]; then
- echo "------------------------------------------------------------------------------"
- echo "[Delete] Clean up Builder Script files"
- echo "------------------------------------------------------------------------------"
-
- # LARGE: find script build and deployment directories
- find . -name "open-source" -type d -prune -exec rm -rf '{}' +
- find . -name "global-s3-assets" -type d -prune -exec rm -rf '{}' +
- find . -name "regional-s3-assets" -type d -prune -exec rm -rf '{}' +
-fi
diff --git a/deployment/clean_s3.py b/deployment/clean_s3.py
deleted file mode 100644
index a30380c4..00000000
--- a/deployment/clean_s3.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-import os
-
-# Third Party Libraries
-import boto3
-
-AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
-AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
-AWS_SESSION_TOKEN = os.environ.get("AWS_SESSION_TOKEN")
-PROFILE = None
-
-if not all([AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN]):
- PROFILE = os.environ.get(
- "AWS_PROFILE",
- input(f"Which AWS profile {boto3.session.Session().available_profiles}: "),
- )
-
-session = boto3.Session(profile_name=PROFILE)
-s3 = session.resource("s3")
-
-
-for bucket in s3.buckets.all():
- if (
- bucket.name.startswith("connected-mobility-solut")
- or bucket.name.startswith("connected-mobility")
- or bucket.name.startswith("cms-dev")
- or bucket.name.startswith("cms-backstage")
- or bucket.name.startswith("awsproton")
- ):
- print(bucket.name)
- bucket.object_versions.delete()
- bucket.objects.delete()
- bucket.delete()
diff --git a/deployment/copy-backstage-templates-to-s3.sh b/deployment/copy-backstage-templates-to-s3.sh
deleted file mode 100755
index 07aba7a8..00000000
--- a/deployment/copy-backstage-templates-to-s3.sh
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/bin/bash
-
-base_directory=$PWD
-aws_account=`aws sts get-caller-identity --query "Account" --output text`
-aws_region=${AWS_REGION:-$(aws configure get region --output text)}
-
-if [[ -z "$aws_region" ]]; then
- echo "*************************"
- echo "Unable to identify AWS_REGION, please add AWS_REGION to environment variables"
- echo "*************************"
- exit 1
-fi
-
-bucket_name=${CMS_RESOURCE_BUCKET:-"${aws_account}-cms-resources-${aws_region}"}
-solution_version=${CMS_SOLUTION_VERSION:-"v0.0.0"}
-
-aws s3 mb s3://${bucket_name}
-
-s3_templates_base_prefix="${solution_version}/backstage/templates"
-
-while IFS= read -r -d '' file; do
- # single filename is in $file
-
- cd $file
- module_name="$(basename $file)"
-
-
- s3_key="${s3_templates_base_prefix}/${module_name}.yaml"
-
- aws s3api put-object \
- --bucket ${bucket_name} \
- --key "${s3_key}" \
- --body ./template.yaml \
- --expected-bucket-owner ${aws_account} \
- > /dev/null #Only output errors to prevent noise
-
- echo Module "'${module_name}'": Uploaded backstage template to "'s3://${bucket_name}/${s3_key}'"
-
- cd $base_directory
-
-done < <(find ./templates/modules -type d -mindepth 1 -maxdepth 1 -print0)
diff --git a/deployment/create-proton-service-templates.sh b/deployment/create-proton-service-templates.sh
deleted file mode 100755
index ac5dd0b8..00000000
--- a/deployment/create-proton-service-templates.sh
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/bin/bash
-
-base_directory=$PWD
-aws_account=`aws sts get-caller-identity --query "Account" --output text`
-aws_region=${AWS_REGION:-$(aws configure get region --output text)}
-
-if [[ -z "$aws_region" ]]; then
- echo "*************************"
- echo "Unable to identify AWS_REGION, please add AWS_REGION to environment variables"
- echo "*************************"
- exit 1
-fi
-
-bucket_name=${CMS_RESOURCE_BUCKET:-"${aws_account}-cms-resources-${aws_region}"}
-solution_version=${CMS_SOLUTION_VERSION:-"v0.0.0"}
-
-function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }
-
-aws s3 mb s3://${bucket_name}
-
-s3_base_prefix="${solution_version}/modules"
-tar_name="service-template"
-
-while IFS= read -r -d '' file; do
- # single filename is in $file
- cd $file
- module_name="$(basename $file)"
-
- s3_service_template_base_prefix="${s3_base_prefix}/${module_name}/proton"
-
- # Scan bucket for current versions, upload 1 patch version higher than greatest current version
- highest_version="1.0.0"
- for path in $(aws s3 ls s3://${bucket_name}/${s3_service_template_base_prefix}/${tar_name}); do
- if [[ "$path" == "${tar_name}-"* ]]; then
-
- version=$(echo "$path" | perl -pe '($_)=/([0-9]+([.][0-9]+)+)/')
-
- if [ $(version $version) -ge $(version $highest_version) ];
- then
- highest_version=$(echo $version | awk -F. '/[0-9]+\./{$NF++;print}' OFS=.)
- fi
- fi
- done
-
- # Create the service template compressed file
- tar_full_name="${tar_name}-${highest_version}.tar.gz"
- tar czf ../../../${tar_full_name} \
- --exclude "node_modules" \
- --exclude "cdk.out" \
- --exclude ".venv" \
- --exclude ".mypy_cache" \
- --exclude ".vscode" \
- --exclude "build" \
- --exclude ".git" \
- --exclude "global-s3-assets" \
- --exclude "regional-s3-assets" \
- ./
-
- # # Upload package to s3
- cd $base_directory
- s3_key="${s3_service_template_base_prefix}/${tar_full_name}"
-
- aws s3api put-object \
- --bucket ${bucket_name} \
- --key "${s3_key}" \
- --body ./${tar_full_name} \
- --expected-bucket-owner ${aws_account} \
- > /dev/null #Only output errors to prevent noise
-
- echo Module "'${module_name}'": Uploaded proton service template "'${highest_version}'" to "'s3://${bucket_name}/${s3_key}'"
-
- rm ./$tar_name-$highest_version.tar.gz
-
-done < <(find ./templates/modules -type d -mindepth 1 -maxdepth 1 -print0)
diff --git a/deployment/detect-empty-files.sh b/deployment/detect-empty-files.sh
deleted file mode 100755
index 5df29592..00000000
--- a/deployment/detect-empty-files.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/bash
-
-showHelp() {
-# `cat << EOF` This means that cat should stop reading when EOF is detected
-cat << EOF
-Usage: ./deployment/detect-empty-files.sh --help
-
-Detect empty files in this project. Deployment of
-the stack will fail if there are empty files.
-
--h, --help Display help
-
-EOF
-# EOF is found above and hence cat command stops reading.
-# This is equivalent to echo but much neater when printing out.
-}
-
-while [[ $# -gt 0 ]]
-do
-key="$1"
-case $key in
- -h|--help)
- showHelp
- exit 0
- ;;
-esac
-done
-
-empty_files_found=""
-
-for file in `git ls-files`
-do
- if [[ -f "$file" && ! -s "$file" ]]; then
- empty_files_found="yes"
- echo "$file is empty!"
- fi
-done
-
-if [[ $empty_files_found == "yes" ]]; then
- echo "############################################"
- echo "Empty files detected!"
- echo "############################################"
- exit 1;
-fi
diff --git a/deployment/determine-bucket-region.sh b/deployment/determine-bucket-region.sh
new file mode 100755
index 00000000..d9afd814
--- /dev/null
+++ b/deployment/determine-bucket-region.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+cache_file="${TMPDIR:-/tmp/}${BUCKET}"
+[ -f "$cache_file" ] && cat "$cache_file" && exit 0
+
+url="https://${BUCKET}.s3.amazonaws.com"
+status_code=$(curl -s -o /dev/null -w "%{http_code}" -I "$url")
+
+if [ "$status_code" -eq 404 ]; then
+ bucket_region=${AWS_REGION};
+elif [ "$status_code" -eq 200 ] || [ "$status_code" -eq 401 ] || [ "$status_code" -eq 403 ]; then
+ bucket_region=$(curl -sI "$url" | grep x-amz-bucket-region | awk '{print $2}' | tr -d '\r');
+ if [ -z "$bucket_region" ]; then
+ bucket_region=${AWS_REGION};
+ fi
+fi
+
+echo "$bucket_region" > "$cache_file"
+# Print the bucket region
+echo "$bucket_region"
diff --git a/deployment/module-build/build-acdp-assets.sh b/deployment/module-build/build-acdp-assets.sh
new file mode 100755
index 00000000..471bebfd
--- /dev/null
+++ b/deployment/module-build/build-acdp-assets.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+
+showHelp() {
+cat << EOF
+Usage: Call this script from a module's ./deployment/build-s3-dist.sh
+
+build and stage a module's ACDP assets (templates/docs)
+EOF
+}
+
+script_dir="$(dirname "$(realpath "$0")")"
+dot_acdp_dir="$MODULE_ROOT_DIR/.acdp"
+mkdocs_staging_dir="$STAGING_DIST_DIR/mkdocs"
+backstage_template_dir="$REGIONAL_ASSETS_DIR/backstage/templates"
+backstage_acdp_assets_dir="$REGIONAL_ASSETS_DIR/backstage/acdp/${MODULE_NAME}/.acdp"
+backstage_docs_dir="$REGIONAL_ASSETS_DIR/backstage/docs"
+backstage_docs_assets_dir="$backstage_docs_dir/components/${MODULE_NAME}"
+
+mkdir -p "$mkdocs_staging_dir"
+mkdir -p "$backstage_template_dir"
+mkdir -p "$backstage_acdp_assets_dir"
+mkdir -p "$backstage_docs_assets_dir"
+
+printf "%b[Backstage] Copying and Updating Backstage discoverable assets\n%b" "${GREEN}" "${NC}"
+python3 "${script_dir}/script_acdp_template_update.py"
+
+cp "$dot_acdp_dir/deploy.buildspec.yaml" "$backstage_acdp_assets_dir/deploy.buildspec.yaml"
+cp "$dot_acdp_dir/update.buildspec.yaml" "$backstage_acdp_assets_dir/update.buildspec.yaml"
+cp "$dot_acdp_dir/teardown.buildspec.yaml" "$backstage_acdp_assets_dir/teardown.buildspec.yaml"
+
+printf "%b[Docs] Generating mkdocs site assets\n%b" "${GREEN}" "${NC}"
+if [ -f "$MODULE_ROOT_DIR/mkdocs.yml" ]; then
+ mkdir -p "$mkdocs_staging_dir/docs";
+ cp -r "$MODULE_ROOT_DIR"/README.md "$mkdocs_staging_dir/docs/index.md";
+ if [ -d "$MODULE_ROOT_DIR/documentation" ]; then
+ cp -r "$MODULE_ROOT_DIR/documentation" "$mkdocs_staging_dir/docs";
+ rm -rf "$mkdocs_staging_dir/docs/documentation/internal"
+ fi
+
+ mkdocs build --clean --site-dir "$mkdocs_staging_dir/site" --config-file "$mkdocs_staging_dir/mkdocs.yml";
+
+ printf "%b[Docs] Copying mkdocs assets\n%b" "${GREEN}" "${NC}";
+ cp -r "$mkdocs_staging_dir/." "$backstage_docs_assets_dir";
+else
+ echo "Module $MODULE_NAME has no mkdocs.yml file in root, skipping mkdocs build"
+fi
diff --git a/deployment/module-build/build-cdk-assets.sh b/deployment/module-build/build-cdk-assets.sh
new file mode 100755
index 00000000..615f40cf
--- /dev/null
+++ b/deployment/module-build/build-cdk-assets.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+
+showHelp() {
+cat << EOF
+Usage: Call this script from a module's ./deployment/build-s3-dist.sh
+
+stage a module's lambda assets
+EOF
+}
+
+lambda_handlers_base_dir="${LAMBDA_HANDLERS_BASE_DIR:-$MODULE_ROOT_DIR/source/handlers}"
+lambda_zip_output_path="${LAMBDA_ZIP_OUTPUT_PATH:-$MODULE_ROOT_DIR/dist/lambda}"
+
+# rm -rf "$lambda_zip_output_path"
+# mkdir -p "$lambda_zip_output_path"
+
+printf "%b\n[Init] Install dependencies for cdk-solution-helper\n%b" "${GREEN}" "${NC}"
+npm ci --prefix "$DEPLOYMENT_DIR/cdk-solution-helper"
+
+
+printf "%b[Build] Build project specific assets\n%b" "${GREEN}" "${NC}"
+while IFS= read -r lambda_dir; do
+ lambda_dir_name="$(basename "$lambda_dir")"
+
+ printf "%s\n" "Building lambda dist: ${lambda_dir}"
+ # Zip lambda source code into folder
+ cd "$lambda_dir"
+ zip -r "$lambda_zip_output_path/$lambda_dir_name.zip" . > /dev/null
+done < <(find "$lambda_handlers_base_dir" -not -path "*__pycache__*" -mindepth 1 -maxdepth 1 -type d)
+
+printf "%b[Synth] Synthesize Stack\n%b" "${GREEN}" "${NC}"
+cd "$MODULE_ROOT_DIR"
+
+# Run cdk synth to generate CloudFormation template
+# JSII_RUNTIME_PACKAGE_CACHE_ROOT is defined so lock collisions don't occur when modules are running concurrently
+# - RuntimeError: EEXIST: file already exists, open '/.cache//aws-cdk-lib/2.130.0/.lock'
+# - https://github.com/aws/jsii/blob/main/packages/%40jsii/kernel/src/tar-cache/default-cache-root.ts
+JSII_RUNTIME_PACKAGE_CACHE_ROOT="$MODULE_ROOT_DIR/.cdk_cache" cdk synth --output="$STAGING_DIST_DIR" >> /dev/null
+
+printf "%b[Packing] Template artifacts\n%b" "${GREEN}" "${NC}"
+rm -f "$STAGING_DIST_DIR/tree.json"
+rm -f "$STAGING_DIST_DIR/manifest.json"
+rm -f "$STAGING_DIST_DIR/cdk.out"
+
+for f in "$STAGING_DIST_DIR"/*.template.json; do
+ mv "$f" "${f%.template.json}.template";
+ mv "${f%.template.json}.template" "$GLOBAL_ASSETS_DIR";
+done
+
+cd "$DEPLOYMENT_DIR/cdk-solution-helper"
+node index
+cd "$MODULE_ROOT_DIR"
+
+printf "%b[Packing] Updating placeholders\n%b" "${GREEN}" "${NC}"
+sedi=(-i)
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ sedi=(-i "")
+fi
+
+for file in "$GLOBAL_ASSETS_DIR"/*.template
+do
+ sed "${sedi[@]}" -E "s/\"\/([^asset][a-z0-9]+.zip)\"/\"\/asset\1\"/g" "$file"
+done
+
+
+printf "%b[Packing] Source code artifacts\n%b" "${GREEN}" "${NC}"
+# For each asset.*.zip source code artifact in the temporary /staging folder
+while IFS= read -r f; do
+ # Rename the artifact, removing the period for handler compatibility
+ zip_file_name="$(basename "$f")"
+ modified_zip_file_name="${zip_file_name/asset\./asset}"
+
+ # Copy the artifact from /staging to /regional-s3-assets
+ mv "$f" "$REGIONAL_ASSETS_DIR/$modified_zip_file_name"
+done < <(find "$STAGING_DIST_DIR" -name "*.zip" -mindepth 1 -maxdepth 1 -type f)
+
+while IFS= read -r d; do
+ # Rename the artifact, removing the period for handler compatibility
+ dir_name="$(basename "$d")"
+ modified_dir_name="${dir_name/\./}"
+
+ # Zip artifacts from asset folder
+ cd "$d"
+ zip -r "$STAGING_DIST_DIR/$modified_dir_name.zip" . > /dev/null
+ cd "$MODULE_ROOT_DIR"
+
+ # Copy the zipped artifact from /staging to /regional-s3-assets
+ mv "$STAGING_DIST_DIR/$modified_dir_name.zip" "$REGIONAL_ASSETS_DIR"
+
+ # Remove the old artifacts from /staging
+ rm -rf "$d"
+done < <(find "$STAGING_DIST_DIR" -mindepth 1 -maxdepth 1 -type d)
+
+printf "%b[Move] Move assets into module specific asset directory\n%b" "${GREEN}" "${NC}"
+mkdir -p "$GLOBAL_ASSETS_DIR/$MODULE_NAME"
+mkdir -p "$REGIONAL_ASSETS_DIR/$MODULE_NAME"
+
+find "$GLOBAL_ASSETS_DIR" -name "*.template" -maxdepth 1 -exec mv {} "$GLOBAL_ASSETS_DIR/$MODULE_NAME/" \;
+find "$REGIONAL_ASSETS_DIR" -name "*.zip" -maxdepth 1 -exec mv {} "$REGIONAL_ASSETS_DIR/$MODULE_NAME/" \;
diff --git a/deployment/module-build/script_acdp_template_update.py b/deployment/module-build/script_acdp_template_update.py
new file mode 100644
index 00000000..1e7ef402
--- /dev/null
+++ b/deployment/module-build/script_acdp_template_update.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import argparse
+import os
+import traceback
+from typing import Any, Dict, List, cast
+from urllib.parse import urljoin
+
+# Third Party Libraries
+import yaml
+from yaml.loader import SafeLoader
+
+module_name = os.environ["MODULE_NAME"]
+module_description = os.environ["MODULE_DESCRIPTION"]
+global_asset_bucket = os.environ["GLOBAL_ASSET_BUCKET_NAME"]
+global_asset_bucket_region = os.environ["GLOBAL_ASSET_BUCKET_REGION"]
+s3_asset_key_prefix = os.environ["S3_ASSET_KEY_PREFIX"]
+stack_template_name = os.environ["STACK_TEMPLATE_NAME"]
+
+p = argparse.ArgumentParser()
+p.add_argument("--component-template-path", default="./.acdp/template.yaml")
+p.add_argument("--mkdocs-yml-path", default="./mkdocs.yml")
+p.add_argument(
+ "--component-template-output-path",
+ default=f"deployment/regional-s3-assets/backstage/templates/{module_name}.template.yaml",
+)
+p.add_argument(
+ "--mkdocs-yml-output-path",
+ default=f"{os.environ.get('STAGING_DIST_DIR', 'deployment/staging')}/mkdocs/mkdocs.yml",
+)
+
+
+def env_constructor(loader: SafeLoader, node: yaml.Node) -> str:
+ value = str(loader.construct_scalar(cast(yaml.ScalarNode, node)))
+ return os.environ[value]
+
+
+yaml.add_constructor(tag="!ENV", constructor=env_constructor, Loader=SafeLoader)
+
+
+def generate_s3_https_url(
+ bucket_name: str, region: str, key_prefix: str, key: str
+) -> str:
+ if region == "us-east-1": # For us-east-1, the region is omitted in the URL
+ bucket_url = f"https://{bucket_name}.s3.amazonaws.com"
+ else:
+ bucket_url = f"https://{bucket_name}.s3.{region}.amazonaws.com"
+
+ url_with_prefix = urljoin(bucket_url, key_prefix)
+ full_url = urljoin(url_with_prefix, key)
+ return full_url
+
+
+def set_nested_json_key_value(
+ json: Dict[str, Any], path: List[str], value: Any
+) -> None:
+ ptr = json
+ for index, key in enumerate(path):
+ if index == len(path) - 1:
+ ptr[key] = value
+ elif key not in ptr:
+ ptr[key] = {}
+ ptr = ptr[key]
+
+
+def update_template(
+ component_template_path: str, component_template_output_path: str
+) -> None:
+ with open(component_template_path, "r", encoding="utf-8") as stream:
+ try:
+ template = yaml.safe_load(stream)
+ except yaml.YAMLError:
+ print(f"Error parsing {component_template_path}")
+ print(traceback.format_exc())
+ return
+
+ template["metadata"]["name"] = module_name
+ template["metadata"]["description"] = module_description
+
+ for index, form_page in enumerate(template["spec"]["parameters"]):
+ if form_page["properties"].get("componentId"):
+ set_nested_json_key_value(
+ json=template["spec"]["parameters"][index],
+ path=["properties", "componentId", "default"],
+ value=module_name,
+ )
+ if form_page["properties"].get("description"):
+ set_nested_json_key_value(
+ json=template["spec"]["parameters"][index],
+ path=["properties", "description", "default"],
+ value=module_description,
+ )
+
+ for index, step in enumerate(template["spec"]["steps"]):
+ if step["action"] == "aws:s3:catalog:write":
+ set_nested_json_key_value(
+ json=template["spec"]["steps"][index],
+ path=["input", "entity", "metadata", "labels", "templateName"],
+ value=module_name,
+ )
+ set_nested_json_key_value(
+ json=template["spec"]["steps"][index],
+ path=[
+ "input",
+ "entity",
+ "metadata",
+ "annotations",
+ "backstage.io/techdocs-entity",
+ ],
+ value=f"component:default/{module_name}-docs",
+ )
+ elif step["action"] == "aws:acdp:configure":
+ for action_index, action_input in enumerate(
+ step["input"]["buildParameters"]
+ ):
+ if action_input["name"] == "CFN_TEMPLATE_URL":
+ cfn_s3_url = generate_s3_https_url(
+ bucket_name=global_asset_bucket,
+ region=global_asset_bucket_region,
+ key_prefix=s3_asset_key_prefix,
+ key=f"{module_name}/{stack_template_name}",
+ )
+ set_nested_json_key_value(
+ json=template["spec"]["steps"][index]["input"][
+ "buildParameters"
+ ][action_index],
+ path=["value"],
+ value=cfn_s3_url,
+ )
+
+ with open(component_template_output_path, "w", encoding="utf-8") as stream:
+ yaml.dump(template, stream, width=150, indent=2)
+
+
+def update_mkdocs_yml(
+ mkdocs_yml_path: str,
+ mkdocs_yml_output_path: str,
+) -> None:
+ with open(mkdocs_yml_path, "r", encoding="utf-8") as stream:
+ try:
+ mkdocs_yml = yaml.safe_load(stream)
+ except yaml.YAMLError:
+ print(f"Error parsing {mkdocs_yml_path}")
+ print(traceback.format_exc())
+ return
+
+ with open(mkdocs_yml_output_path, "w", encoding="utf-8") as stream:
+ yaml.dump(mkdocs_yml, stream, width=150, indent=2)
+
+
+if __name__ == "__main__":
+ args = p.parse_args()
+ update_template(
+ component_template_path=args.component_template_path,
+ component_template_output_path=args.component_template_output_path,
+ )
+
+ update_mkdocs_yml(
+ mkdocs_yml_path=args.mkdocs_yml_path,
+ mkdocs_yml_output_path=args.mkdocs_yml_output_path,
+ )
diff --git a/deployment/run-cfn-nag.sh b/deployment/run-cfn-nag.sh
deleted file mode 100755
index c20bff37..00000000
--- a/deployment/run-cfn-nag.sh
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/bin/bash
-
-showHelp() {
-# `cat << EOF` This means that cat should stop reading when EOF is detected
-cat << EOF
-Usage: ./deployment/run-cfn-nag.sh --help
-
-Run "cdk-nag" and cfn-nag in this project.
-
--h, --help Display help
-
--dl, --deny-list-path Pass the file name which contains cfn-nag rules to suppress
-
-EOF
-# EOF is found above and hence cat command stops reading. This is equivalent to echo but much neater when printing out.
-}
-
-# $@ is all command line parameters passed to the script.
-# -o is for short options like -v
-# -l is for long options with double dash like --version
-# the comma separates different long options
-# -a is for long options with single dash like -version
-options=$(getopt -l "help,deny-list-path,no-nested" -o "hRN" -a -- "$@")
-deny_list_path=""
-run_nested_commands=true
-
-while true
-do
- case "$1" in
- -h|--help)
- showHelp
- exit 0
- ;;
- -dl|--deny-list-path)
- deny_list_path="$2"
- shift
- ;;
- -n|--no-nested)
- run_nested_commands=false
- ;;
- *)
- shift
- break;;
- esac
- shift
-done
-
-# Activate local environment
-echo "===================================================="
-echo "Activating venv found in $PWD"
-echo "===================================================="
-source ./.venv/bin/activate
-
-[ "$DEBUG" == 'true' ] && set -x
-
-cdk_out_dir=$PWD/cdk.out
-
-# Synthesize the latest stack template files
-rm -rf $cdk_out_dir
-make synth
-did_cdk_synth_fail=$?
-
-did_cmdp_nag_failure_occur=0
-if [[ $did_cdk_synth_fail -ne 0 ]]
-then
- echo "===================================================="
- echo "CDK SYNTH failed, can not perform CFN NAG Scan"
- echo "===================================================="
- did_cmdp_nag_failure_occur=1
-else
- # Loop through all files with extension .template.json inside the cdk.out folder
- for file in "${cdk_out_dir}"/*.template.json
- do
- # Check if the file exists and is a file (not a directory)
- if [[ -f "${file}" ]]; then
- # Run cfn_nag on the file
- if [ "$deny_list_path" == "" ]; then
- output=$(cfn_nag "${file}" 2>&1)
- else
- output=$(cfn_nag "${file}" --deny-list-path=$deny_list_path 2>&1)
- fi
- # Check if there are any warnings in the output
- if [[ "${output}" == *"WARN"* ]]; then
- # Set the warnings_exist flag to true
- warnings_exist=true
- fi
- # Check if there are any failures in the output
- if [[ "${output}" == *"FAIL"* ]]; then
- # Set the failures_exist flag to true
- failures_exist=true
- fi
- echo "$output"
- fi
- done
- # If there were any warnings or failures, note them, but don't exit yet so the rest of the module scripts will run.
- if [[ "${warnings_exist}" = true || "${failures_exist}" = true ]]; then
- echo "===================================================="
- echo "CFN NAG Scan failed"
- echo "===================================================="
- did_cmdp_nag_failure_occur=1
- fi
-fi
-
-# <=====UNIQUE TO TOP LEVEL SCRIPT=====>
-# Run the same script for all of the individual modules
-did_module_script_failure_occur=0
-if [ $run_nested_commands = true ]
-then
- $PWD/deployment/run-module-scripts.sh $(basename $0) $@
- did_module_script_failure_occur=$?
-fi
-# <=====UNIQUE TO TOP LEVEL SCRIPT=====>
-
-# Check if module or cmdp failures occured, and exit accordingly
-if [[ $did_module_script_failure_occur -ne 0 || $did_cmdp_nag_failure_occur -ne 0 ]]
-then
- exit 1
-fi
diff --git a/deployment/run-clean-build-artifacts.sh b/deployment/run-clean-build-artifacts.sh
new file mode 100755
index 00000000..6b14b8c5
--- /dev/null
+++ b/deployment/run-clean-build-artifacts.sh
@@ -0,0 +1,116 @@
+#!/bin/bash
+
+showHelp() {
+cat << EOF
+Usage: ./deployment/run-clean-build-artifacts.sh --help
+
+Clean build artifacts.
+
+-r, --release-build Remove the release build files
+
+-d, --dependencies Remove the dependencies and virtual environments
+
+-l, --lock-files Remove the lock files
+
+-a, --all Remove all artifacts
+
+EOF
+}
+
+release_build=""
+dependencies=""
+lock_files=""
+
+while [[ $# -gt 0 ]]
+do
+key="$1"
+case $key in
+ -h|--help)
+ showHelp
+ exit 0
+ ;;
+ -r|--release-build)
+ release_build="yes"
+ shift
+ ;;
+ -d|--dependencies)
+ dependencies="yes"
+ shift
+ ;;
+ -l|--lock-files)
+ lock_files="yes"
+ shift
+ ;;
+ -a|--all)
+ release_build="yes"
+ dependencies="yes"
+ lock_files="yes"
+ shift
+ ;;
+ *)
+ shift
+esac
+done
+
+# MEDIUM: find javascript build directories
+printf "%b[Delete] Cleaning up Javascript build files%b\n" "${RED}" "${NC}"
+find . -name "dist" -type d -not -path "**/node_modules/*" -prune -exec rm -rf '{}' +
+find . -name "dist-types" -type d -not -path "**/node_modules/*" -prune -exec rm -rf '{}' +
+find . -name "build" -type d -not -path "**/node_modules/*" -not -path "**/.venv/*" -prune -exec rm -rf '{}' +
+
+if [[ $dependencies == "yes" ]]; then
+ # MEDIUM: find javascript install directories
+ printf "%b[Delete Dependencies] Cleaning up Javascript dependencies%b\n" "${RED}" "${NC}"
+ find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
+fi
+
+if [[ $lock_files == "yes" ]]; then
+ # SMALL: find javascript lock files, these are hovering around 1-2MB
+ printf "%b[Delete Lock Files] Cleaning up Javascript lock files%b\n" "${RED}" "${NC}"
+ find . -name "package-lock.json" -type f -prune -exec rm -rf '{}' +
+ find . -name "yarn.lock" -type f -prune -exec rm -rf '{}' +
+fi
+
+# LARGE: find cdk build directories
+printf "%b[Delete] Cleaning up CDK build files%b\n" "${RED}" "${NC}"
+find . -name "cdk.out" -type d -prune -exec rm -rf '{}' +
+find . -name ".cdk_cache" -type d -prune -exec rm -rf '{}' +
+find . -name "generated_models" -type d -prune -exec rm -rf '{}' +
+
+# SMALL: find python noise
+printf "%b[Delete] Cleaning up Python files%b\n" "${RED}" "${NC}"
+find . -name "__pycache__" -type d -prune -exec rm -rf '{}' +
+find . -name ".pytest_cache" -type d -prune -exec rm -rf '{}' +
+find . -name ".mypy_cache" -type d -prune -exec rm -rf '{}' +
+find . -name "*.egg-info" -type d -prune -exec rm -rf '{}' +
+find . -name ".coverage" -type d -prune -exec rm -rf '{}' +
+
+if [[ $dependencies == "yes" ]]; then
+ # MEDIUM: find any child virtual environments
+ printf "%b[Delete Dependencies] Cleaning up Python dependencies%b\n" "${RED}" "${NC}"
+ find . -mindepth 2 -name ".venv" -type d -prune -exec rm -rf '{}' +
+fi
+
+if [[ $lock_files == "yes" ]]; then
+ # SMALL: find Pipfile.lock files, these are hovering around 1-2MB
+ printf "%b[Delete Lock Files] Cleaning up Python lock files%b\n" "${RED}" "${NC}"
+ find . -name "Pipfile.lock" -type f -prune -exec rm -rf '{}' +
+fi
+
+# MEDIUM: find layers
+printf "%b[Delete] Cleaning up AWS Lambda dependency layers%b\n" "${RED}" "${NC}"
+find . -name "None" -type d -prune -exec rm -rf '{}' +
+find . -name "*_dependency_layer" -type d -prune -exec rm -rf '{}' +
+
+# MEDIUM: find chalice
+printf "%b[Delete] Cleaning up AWS Chalice files%b\n" "${RED}" "${NC}"
+find . -name "chalice.out" -type d -prune -exec rm -rf '{}' +
+find . -name "deployments" -type d -prune -exec rm -rf '{}' +
+
+if [[ $release_build == "yes" ]]; then
+ # LARGE: find script build and deployment directories
+ printf "%b[Delete Release Build] Cleaning up release build files%b\n" "${RED}" "${NC}"
+ find . -name "open-source" -type d -prune -exec rm -rf '{}' +
+ find . -name "global-s3-assets" -type d -prune -exec rm -rf '{}' +
+ find . -name "regional-s3-assets" -type d -prune -exec rm -rf '{}' +
+fi
diff --git a/deployment/run-detect-empty-files.sh b/deployment/run-detect-empty-files.sh
new file mode 100755
index 00000000..b677be60
--- /dev/null
+++ b/deployment/run-detect-empty-files.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+
+showHelp() {
+cat << EOF
+Usage: ./deployment/run-detect-empty-files.sh --help
+
+Detect empty files in this project. Deployment of
+the stack will fail if there are empty files.
+
+EOF
+}
+
+while [[ $# -gt 0 ]]
+do
+key="$1"
+case $key in
+ -h|--help)
+ showHelp
+ exit 0
+ ;;
+esac
+done
+
+empty_files_found=""
+
+for file in $(git ls-files)
+do
+ if [[ -f "$file" && ! -s "$file" ]]; then
+ empty_files_found="yes"
+ echo "$file is empty!"
+ fi
+done
+
+if [[ $empty_files_found == "yes" ]]; then
+ echo "############################################"
+ echo "Empty files detected!"
+ echo "############################################"
+ exit 1;
+fi
diff --git a/deployment/run-module-scripts.sh b/deployment/run-module-scripts.sh
deleted file mode 100755
index 11ebb795..00000000
--- a/deployment/run-module-scripts.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/bin/bash
-
-# Find and execute all similarly named scripts, but not if in cdk.out or itself (infinite loops are fun)
-base_directory=$PWD
-
-# If a module failure occurs, we will exit with a failed status code at the end, but we still want to run every module's script
-did_module_script_failure_occur=0
-
-while IFS= read -r -d '' file; do
- # The module specific script file name is in $file
- echo ""
- echo "===================================================="
- echo "Running $file"
- echo "===================================================="
- echo ""
-
- $file ${@:2}
- most_recent_module_script_exit_code=$?
-
- # Check the result of the script and mark if a failure is identified
- if [[ $most_recent_module_script_exit_code -ne 0 ]]
- then
- echo ""
- echo "===================================================="
- echo "Module Script Failure: $file FAILED in $base_directory"
- echo "===================================================="
- echo ""
- did_module_script_failure_occur=1
- fi
-
- # module script might have called cd, bring us back
- cd $base_directory
-done < <(find . -name "$1" -not -path "**/cdk.out/*" -not -path "./deployment/*" -not -path "**/node_modules/*" -print0)
-
-# We return whether a module script occured so we can exit appropriately in the higher level script
-exit $did_module_script_failure_occur
diff --git a/deployment/run-shellcheck.sh b/deployment/run-shellcheck.sh
new file mode 100755
index 00000000..8e9deeab
--- /dev/null
+++ b/deployment/run-shellcheck.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+if command -v "shellcheck" >/dev/null 2>&1; then
+ shellcheck "$@"
+else
+ echo 'Your system does not have shellcheck, instructions are here https://github.com/koalaman/shellcheck#installing'
+ exit 1
+fi
diff --git a/deployment/run-unit-tests.sh b/deployment/run-unit-tests.sh
index e3230f7f..0c50a771 100755
--- a/deployment/run-unit-tests.sh
+++ b/deployment/run-unit-tests.sh
@@ -1,158 +1,10 @@
#!/bin/bash
-#
-# This assumes all of the OS-level configuration has been completed and git repo has already been cloned
-#
-# This script should be run from the repo's deployment directory
-# ./run-unit-tests.sh
-#
-showHelp() {
-# `cat << EOF` This means that cat should stop reading when EOF is detected
-cat << EOF
-Usage: ./deployment/run-unit-tests.sh --help
-Run unit tests in this project.
+# This file is not used, but it is required by the pipeline checks. Specifically viperlight pubcheck.
+# This can be replaced with `touch ./deployment/run-all-tests.sh` in the buildspec.yaml which also
+# gets picked up on the check that makes sure that run-all-tests.sh is "called" in the buildspec.
+# It doesn't actually check that it is called, just does a basic grep.
--h, --help Display help
-
--n, --no-nested Don't run module level versions of this script
-
--r, --no-report Don't generate the report, this is mainly used for pre-commit
-
-EOF
-# EOF is found above and hence cat command stops reading. This is equivalent to echo but much neater when printing out.
-}
-
-# $@ is all command line parameters passed to the script.
-# -o is for short options like -v
-# -l is for long options with double dash like --version
-# the comma separates different long options
-# -a is for long options with single dash like -version
-options=$(getopt -l "help,no-nested,no-report" -o "hnr" -a -- "$@")
-generate_report=true
-run_nested_commands=true
-
-while true
-do
- case "$1" in
- -h|--help)
- showHelp
- exit 0
- ;;
- -n|--no-nested)
- run_nested_commands=false
- ;;
- -r|--no-report)
- generate_report=false
- ;;
- *)
- shift
- break;;
- esac
- shift
-done
-
-# Activate local environment
-echo "===================================================="
-echo "Activating venv found in $PWD"
-echo "===================================================="
-source ./.venv/bin/activate
-
-[ "$DEBUG" == 'true' ] && set -x
-
-# Get reference for all important folders
-project_dir=$PWD
-source_dir="$project_dir/source"
-tests_dir="$source_dir/tests"
-metrics_tests_dir="$source_dir/infrastructure/handlers/metrics/app/tests"
-backstage_tests_dir="$source_dir/backstage/cdk/source/tests"
-coverage_reports_top_path="$source_dir/tests/coverage-reports"
-backstage_dir="$source_dir/backstage"
-backstage_frontend_dir="$source_dir/backstage/packages/app"
-backstage_backend_dir="$source_dir/backstage/packages/backend"
-
-rm -rf $project_dir/.coverage
-
-# Run test on package and save results to coverage_report_path in xml format
-python_coverage_report="$coverage_reports_top_path/coverage.xml"
-if [ $generate_report = true ]
-then
- pytest $tests_dir $backstage_tests_dir $metrics_tests_dir \
- --cov=$source_dir \
- --cov-report=term \
- --cov-report=xml:$python_coverage_report \
- --cov-config=$project_dir/.coveragerc \
- --snapshot-update
-else
- pytest $tests_dir $backstage_tests_dir $metrics_tests_dir \
- --cov=$source_dir \
- --cov-report=term \
- --cov-config=$project_dir/.coveragerc
-fi
-did_cmdp_failure_occur=$?
-
-# Check the result of the test and echo if a failure was detected. Don't exit yet so the rest of the module tests will run.
-if [[ $did_cmdp_failure_occur -ne 0 ]]
-then
- echo "===================================================="
- echo "test FAILED for $source_dir"
- echo "===================================================="
-fi
-
-# Linux and MacOS have different ways of calling the sed command for in-place editing.
-# MacOS takes a mandatory argument for the -i flag whereas linux does not.
-sedi=(-i)
-if [[ "$OSTYPE" == "darwin"* ]]; then
- sedi=(-i "")
-fi
-# The pytest coverage report xml generated has the absolute path of the files
-# when reporting coverage. Replace the absolute path with the relative path from
-# the project's root directory so that SonarQube can understand the coverage report.
-sed "${sedi[@]}" -e "s,
- """
- ),
- sms_message="Hello {username}, your temporary password for CMS Backstage is {####}",
- ),
- password_policy=aws_cognito.PasswordPolicy(
- min_length=12,
- require_lowercase=True,
- require_uppercase=True,
- require_digits=True,
- require_symbols=True,
- temp_password_validity=Duration.days(1),
- ),
- device_tracking=aws_cognito.DeviceTracking(
- challenge_required_on_new_device=True,
- device_only_remembered_on_user_prompt=True,
- ),
- )
- aws_cognito.CfnUserPoolUser(
- self,
- "cognito-admin-user",
- user_pool_id=backstage_user_pool.user_pool_id,
- desired_delivery_mediums=["EMAIL"],
- force_alias_creation=True,
- user_attributes=[
- {
- "name": "email",
- "value": aws_ssm.StringParameter.value_for_string_parameter(
- self,
- f"/{BackstageConstants.STAGE}/cms/admin-email",
- ),
- },
- {"name": "email_verified", "value": "true"},
- ],
- username=aws_ssm.StringParameter.value_for_string_parameter(
- self,
- f"/{BackstageConstants.STAGE}/cms/username",
- ),
- )
-
- backstage_cluster = aws_ecs.Cluster(
- self,
- "backstage-ecs-cluster",
- vpc=vpc,
- container_insights=True,
- )
-
- task_role = aws_iam.Role(
- self,
- "backstage-task-definition-role",
- role_name=f"{BackstageConstants.STACK_NAME}-{Stack.of(self).region}-backstage-task",
- assumed_by=aws_iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
- inline_policies={
- "s3-backstage-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "s3:GetBucketAcl",
- "s3:GetBucketLocation",
- "s3:GetBucketVersioning",
- "s3:GetObject",
- "s3:GetObjectAcl",
- "s3:GetObjectAttributes",
- "s3:GetObjectVersion",
- "s3:GetObjectVersionAcl",
- "s3:GetObjectVersionTagging",
- "s3:ListAllMyBuckets",
- "s3:ListBucket",
- "s3:ListBucketVersions",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="s3",
- resource=cms_resource_bucket_name_param.string_value,
- resource_name=None,
- account="",
- region="",
- arn_format=ArnFormat.NO_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="s3",
- resource=cms_resource_bucket_name_param.string_value,
- resource_name="*",
- account="",
- region="",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="s3",
- resource=backstage_catalog_bucket_name_param.string_value,
- resource_name=None,
- account="",
- region="",
- arn_format=ArnFormat.NO_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="s3",
- resource=backstage_catalog_bucket_name_param.string_value,
- resource_name=f"{backstage_catalog_bucket_key_prefix_param.string_value}/*",
- account="",
- region="",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- ],
- ),
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=["s3:PutObject"],
- resources=[
- Stack.of(self).format_arn(
- service="s3",
- resource=backstage_catalog_bucket_name_param.string_value,
- resource_name=None,
- account="",
- region="",
- arn_format=ArnFormat.NO_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="s3",
- resource=backstage_catalog_bucket_name_param.string_value,
- resource_name=f"{backstage_catalog_bucket_key_prefix_param.string_value}/*",
- account="",
- region="",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- ],
- ),
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "kms:GenerateDataKey",
- "kms:Decrypt",
- "kms:Encrypt",
- ],
- resources=[
- backstage_catalog_bucket_kms_key_arn.string_value
- ],
- ),
- ]
- ),
- "proton-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "proton:ListServiceInstances",
- ],
- resources=["*"],
- ),
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "proton:GetService",
- "proton:CreateService",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="proton",
- resource="service",
- resource_name="*",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- )
- ],
- ),
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- resources=[
- Stack.of(self).format_arn(
- service="codestar-connections",
- resource="connection",
- resource_name="*",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- )
- ],
- actions=["codestar-connections:PassConnection"],
- conditions={
- "StringEquals": {
- "codestar-connections:PassedToService": "proton.amazonaws.com"
- }
- },
- ),
- ]
- ),
- "cognito-idp-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "cognito-idp:DescribeUserPool",
- "cognito-idp:DescribeUserPoolClient",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="cognito-idp",
- resource="userpool",
- resource_name=backstage_user_pool.user_pool_id,
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- ],
- ),
- ]
- ),
- },
- )
-
- task_definition = aws_ecs.FargateTaskDefinition(
- self,
- "backstage-ecs-fargate-task-definition",
- cpu=1024,
- memory_limit_mib=2048,
- ephemeral_storage_gib=30,
- family=BackstageConstants.STACK_NAME,
- execution_role=task_role,
- task_role=task_role,
- )
-
- pg_admin = aws_secretsmanager.Secret.from_secret_name_v2(
- self,
- "backstage-pg-admin-secret",
- f"/{BackstageConstants.STAGE}/{BackstageConstants.APP_NAME}/backstage_pg_admin",
- )
-
- backstage_log_group_kms_key = aws_kms.Key(
- self,
- "backstage-log-group-kms-key",
- alias="backstage-log-group-kms-key",
- enable_key_rotation=True,
- )
-
- backstage_log_group = aws_logs.LogGroup(
- self,
- "backstage-log-group",
- removal_policy=RemovalPolicy.RETAIN,
- retention=aws_logs.RetentionDays.THREE_MONTHS,
- encryption_key=backstage_log_group_kms_key,
- )
-
- backstage_log_group_kms_key.add_to_resource_policy(
- statement=aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- principals=[
- aws_iam.ServicePrincipal(
- f"logs.{Stack.of(self).region}.amazonaws.com"
- )
- ],
- actions=["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
- resources=["*"],
- )
- )
-
- task_definition.add_container(
- f"{BackstageConstants.STACK_NAME}-container",
- image=aws_ecs.ContainerImage.from_ecr_repository(
- repository=aws_ecr.Repository.from_repository_name(
- self, "backstage-ecr", "backstage"
- ),
- tag=self.node.get_context("backstage-image-tag"),
- ),
- port_mappings=[
- aws_ecs.PortMapping(
- container_port=8080,
- protocol=aws_ecs.Protocol.TCP,
- )
- ],
- container_name=f"{BackstageConstants.STACK_NAME}-backend",
- secrets={
- "BACKSTAGE_NAME": aws_ecs.Secret.from_ssm_parameter(
- cms_backstage_name_param
- ),
- "BACKSTAGE_ORG": aws_ecs.Secret.from_ssm_parameter(
- cms_backstage_org_param
- ),
- "POSTGRES_USER": aws_ecs.Secret.from_secrets_manager(
- pg_admin, "username"
- ),
- "POSTGRES_PASSWORD": aws_ecs.Secret.from_secrets_manager(
- pg_admin, "password"
- ),
- "POSTGRES_HOST": aws_ecs.Secret.from_secrets_manager(pg_admin, "host"),
- "POSTGRES_PORT": aws_ecs.Secret.from_secrets_manager(pg_admin, "port"),
- "BACKEND_SECRET": aws_ecs.Secret.from_secrets_manager(
- aws_secretsmanager.Secret.from_secret_complete_arn(
- self,
- "backend-secret-arn",
- secret_complete_arn=aws_ssm.StringParameter.value_for_string_parameter(
- self,
- f"/{BackstageConstants.STAGE}/cms/secret-arns/backend-secret",
- ),
- )
- ),
- "CMS_RESOURCE_BUCKET_NAME": aws_ecs.Secret.from_ssm_parameter(
- cms_resource_bucket_name_param
- ),
- "CMS_RESOURCE_BUCKET_REGION": aws_ecs.Secret.from_ssm_parameter(
- cms_resource_bucket_region_param
- ),
- "CMS_RESOURCE_BUCKET_TEMPLATE_KEY_PREFIX": aws_ecs.Secret.from_ssm_parameter(
- cms_resource_bucket_template_key_prefix_param
- ),
- "CMS_RESOURCE_BUCKET_TEMPLATE_CHECK_FREQ": aws_ecs.Secret.from_ssm_parameter(
- cms_resource_bucket_template_check_freq_param
- ),
- "BACKSTAGE_CATALOG_BUCKET_NAME": aws_ecs.Secret.from_ssm_parameter(
- backstage_catalog_bucket_name_param
- ),
- "BACKSTAGE_CATALOG_BUCKET_REGION": aws_ecs.Secret.from_ssm_parameter(
- backstage_catalog_bucket_region_param
- ),
- "BACKSTAGE_CATALOG_BUCKET_KEY_PREFIX": aws_ecs.Secret.from_ssm_parameter(
- backstage_catalog_bucket_key_prefix_param
- ),
- },
- environment={
- "WEB_SCHEME": aws_ssm.StringParameter.value_for_string_parameter(
- self,
- f"/{BackstageConstants.STAGE}/cms/web-scheme",
- ),
- "WEB_HOSTNAME": route53_base_domain,
- "WEB_PORT": aws_ssm.StringParameter.value_for_string_parameter(
- self,
- f"/{BackstageConstants.STAGE}/cms/web-port",
- ),
- "BACKEND_SCHEME": aws_ssm.StringParameter.value_for_string_parameter(
- self,
- f"/{BackstageConstants.STAGE}/cms/web-scheme",
- ),
- "BACKEND_HOSTNAME": route53_base_domain,
- "BACKEND_PORT": aws_ssm.StringParameter.value_for_string_parameter(
- self,
- f"/{BackstageConstants.STAGE}/cms/web-port",
- ),
- "NODE_ENV": aws_ssm.StringParameter.value_for_string_parameter(
- self,
- f"/{BackstageConstants.STAGE}/cms/node-env",
- ),
- "COGNITO_USERPOOL_ID": backstage_user_pool.user_pool_id,
- "LOG_LEVEL": aws_ssm.StringParameter.value_for_string_parameter(
- self,
- f"/{BackstageConstants.STAGE}/cms/backstage-log-level",
- ),
- },
- logging=aws_ecs.LogDriver.aws_logs(
- log_group=backstage_log_group,
- stream_prefix=f"{BackstageConstants.STACK_NAME}-logs",
- ),
- )
-
- backstage_fargate_service = aws_ecs.FargateService(
- self,
- "backstage-ecs-fargate-service",
- cluster=backstage_cluster,
- task_definition=task_definition,
- service_name=f"{BackstageConstants.STACK_NAME}-fargate-service",
- desired_count=2,
- min_healthy_percent=50,
- max_healthy_percent=200,
- )
-
- backstage_database_security_group = aws_ec2.SecurityGroup.from_lookup_by_id(
- self,
- "backstage-database-security-group-id",
- security_group_id=aws_ssm.StringParameter.value_from_lookup(
- self,
- f"/{BackstageConstants.STAGE}/{BackstageConstants.APP_NAME}/security-groups/backstage-database-security-group-id",
- ),
- )
-
- backstage_database_security_group.connections.allow_from(
- other=backstage_fargate_service,
- port_range=aws_ec2.Port.tcp(5432),
- description="Allow database access from the backstage fargate service",
- )
-
- load_balancer = aws_elasticloadbalancingv2.ApplicationLoadBalancer(
- self,
- f"{BackstageConstants.STACK_NAME}-alb",
- vpc=vpc,
- vpc_subnets=aws_ec2.SubnetSelection(subnets=vpc.public_subnets),
- load_balancer_name=f"{BackstageConstants.STACK_NAME}-alb",
- internet_facing=True,
- drop_invalid_header_fields=True,
- )
- load_balancer.log_access_logs(
- bucket=aws_s3.Bucket(
- self,
- "backstage-elb-logs-bucket",
- block_public_access=aws_s3.BlockPublicAccess.BLOCK_ALL,
- enforce_ssl=True,
- versioned=True,
- encryption=aws_s3.BucketEncryption.S3_MANAGED,
- ),
- prefix="backstage-alb",
- )
- listener = load_balancer.add_listener(
- "listener",
- port=443,
- ssl_policy=aws_elasticloadbalancingv2.SslPolicy.TLS13_RES,
- )
-
- route53_zone = aws_route53.HostedZone.from_lookup(
- self,
- "backstage-route53-hosted-zone",
- domain_name=route53_zone_name,
- )
-
- # Cognito only supports certificates in us-east-1
- cognito_certificate = aws_certificatemanager.DnsValidatedCertificate(
- self,
- "cognito-certificate",
- hosted_zone=route53_zone,
- region="us-east-1",
- domain_name=route53_base_domain,
- subject_alternative_names=[f"*.{route53_base_domain}"],
- )
-
- # ALB needs certificate in the same region as itself
- listener_certificate = aws_certificatemanager.DnsValidatedCertificate(
- self,
- "alb-listener-certificate",
- hosted_zone=route53_zone,
- region=Stack.of(self).region,
- domain_name=route53_base_domain,
- subject_alternative_names=[f"*.{route53_base_domain}"],
- )
-
- listener.add_certificates(
- "listener-certificates",
- certificates=[
- aws_elasticloadbalancingv2.ListenerCertificate.from_arn(
- listener_certificate.certificate_arn
- )
- ],
- )
- target_group = listener.add_targets(
- "fleet",
- port=443,
- protocol=aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,
- targets=[backstage_fargate_service],
- )
-
- aws_elasticloadbalancingv2.ApplicationListenerRule(
- self,
- "listener-rule",
- priority=1,
- listener=listener,
- conditions=[
- aws_elasticloadbalancingv2.ListenerCondition.path_patterns(["*"])
- ],
- target_groups=[target_group],
- )
-
- root_record = aws_route53.ARecord(
- self,
- "backstage-route53-record",
- zone=route53_zone,
- record_name=f"{route53_base_domain}.",
- target=aws_route53.RecordTarget.from_alias(
- aws_route53_targets.LoadBalancerTarget(load_balancer)
- ),
- )
-
- backstage_user_pool_domain = backstage_user_pool.add_domain(
- "backstage-user-pool-domain",
- custom_domain=aws_cognito.CustomDomainOptions(
- certificate=aws_elasticloadbalancingv2.ListenerCertificate.from_arn( # type: ignore
- cognito_certificate.certificate_arn
- ),
- domain_name=f"auth.{route53_base_domain}",
- ),
- )
- backstage_user_pool_domain.node.add_dependency(root_record)
-
- aws_route53.ARecord(
- self,
- "backstage-route-to-cognito",
- zone=route53_zone,
- record_name=f"auth.{route53_base_domain}.",
- target=aws_route53.RecordTarget.from_alias(
- aws_route53_targets.UserPoolDomainTarget(backstage_user_pool_domain)
- ),
- )
-
- oidc_client = backstage_user_pool.add_client(
- "oidc-client",
- generate_secret=True,
- access_token_validity=Duration.hours(1),
- auth_session_validity=Duration.minutes(3),
- enable_token_revocation=True,
- id_token_validity=Duration.hours(1),
- prevent_user_existence_errors=True,
- refresh_token_validity=Duration.hours(2),
- o_auth=aws_cognito.OAuthSettings(
- flows=aws_cognito.OAuthFlows(
- authorization_code_grant=True,
- ),
- scopes=[aws_cognito.OAuthScope.OPENID],
- callback_urls=[
- f"https://{load_balancer.load_balancer_dns_name}/api/auth/cognito/handler/frame",
- f"https://{load_balancer.load_balancer_dns_name}/oauth2/idpresponse",
- f"https://{route53_base_domain}/api/auth/cognito/handler/frame",
- f"https://{route53_base_domain}/oauth2/idpresponse",
- ],
- ),
- )
- try:
- task_definition.default_container.add_environment( # type: ignore
- "COGNITO_CLIENT_ID", oidc_client.user_pool_client_id
- )
- except AttributeError:
- # for some reason the default container was not found
- print(
- "Default container not found, unable to add COGNITO_CLIENT_ID to the environment"
- )
diff --git a/source/backstage/cdk/source/tests/__init__.py b/source/backstage/cdk/source/tests/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/backstage/cdk/source/tests/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/backstage/cdk/source/tests/conftest.py b/source/backstage/cdk/source/tests/conftest.py
deleted file mode 100644
index 1cb42b94..00000000
--- a/source/backstage/cdk/source/tests/conftest.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# pylint: disable=W0611
-
-# Connected Mobility Solution on AWS
-from .fixtures.fixture_shared import fixture_mock_ssm_params, fixture_template
diff --git a/source/backstage/cdk/source/tests/fixtures/__init__.py b/source/backstage/cdk/source/tests/fixtures/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/backstage/cdk/source/tests/fixtures/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/backstage/cdk/source/tests/fixtures/fixture_shared.py b/source/backstage/cdk/source/tests/fixtures/fixture_shared.py
deleted file mode 100644
index 6a35c789..00000000
--- a/source/backstage/cdk/source/tests/fixtures/fixture_shared.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, Dict, Generator
-from unittest.mock import patch
-
-# Third Party Libraries
-import aws_cdk
-import pytest
-
-# Connected Mobility Solution on AWS
-from ...infrastructure.stacks.stack import BackstageStack
-
-
-@pytest.fixture(name="mock_ssm_params", scope="session")
-def fixture_mock_ssm_params() -> Generator[None, None, None]:
- with patch(
- "aws_cdk.aws_ssm.StringParameter.value_from_lookup",
- new=lambda scope, parameter_name: "TEST",
- ):
- yield
-
-
-@pytest.fixture(name="template", scope="session")
-def fixture_template(mock_ssm_params: Dict[str, Any]) -> aws_cdk.assertions.Template:
- app = aws_cdk.App(
- context={
- "backstage-image-tag": "DUMMY",
- }
- )
- stack = BackstageStack(
- app,
- "test-stack",
- env=aws_cdk.Environment(
- account="test-account-id",
- region="us-west-2",
- ),
- )
- template = aws_cdk.assertions.Template.from_stack(stack)
- return template
diff --git a/source/backstage/cdk/source/tests/infrastructure/__init__.py b/source/backstage/cdk/source/tests/infrastructure/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/backstage/cdk/source/tests/infrastructure/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/backstage/cdk/source/tests/infrastructure/aspects/__init__.py b/source/backstage/cdk/source/tests/infrastructure/aspects/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/backstage/cdk/source/tests/infrastructure/aspects/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/backstage/cdk/source/tests/infrastructure/aspects/test_nag_suppression.py b/source/backstage/cdk/source/tests/infrastructure/aspects/test_nag_suppression.py
deleted file mode 100644
index ce6ae8ae..00000000
--- a/source/backstage/cdk/source/tests/infrastructure/aspects/test_nag_suppression.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from os.path import dirname, realpath
-from typing import Any
-
-# Third Party Libraries
-from aws_cdk import App, Stack, assertions, aws_kms
-from constructs import Construct
-
-# Connected Mobility Solution on AWS
-from ....infrastructure.aspects.backstage_nag_suppression import NagSuppression, NagType
-
-
-class NagTestStack(Stack):
- def __init__(self, scope: Construct, construct_id: str, **kwargs: Any) -> None:
- super().__init__(scope, construct_id, **kwargs)
-
- self.test_key = aws_kms.Key(
- self,
- "nag-test-key",
- enable_key_rotation=True,
- )
-
-
-def test_nag_suppression_cdk_metadata() -> None:
- app = App()
- test_stack = NagTestStack(app, "nag-test-stack")
- cdk_nag_suppression = NagSuppression(
- f"{dirname(realpath(__file__))}/test-cdk-nag-suppression-list.json",
- NagType.CDK_NAG,
- )
- l1_construct = test_stack.test_key.node.default_child
- if l1_construct is not None:
- cdk_nag_suppression.visit(l1_construct)
- template = assertions.Template.from_stack(test_stack)
- template.has_resource(
- "AWS::KMS::Key",
- {
- "Metadata": {
- "cdk_nag": {
- "rules_to_suppress": [
- {"id": "test-cdk-id", "reason": "test-cdk-reason"}
- ]
- }
- }
- },
- )
- else:
- assert False
-
-
-def test_nag_suppression_cfn_metadata() -> None:
- app = App()
- test_stack = NagTestStack(app, "nag-test-stack")
- cfn_nag_suppression = NagSuppression(
- f"{dirname(realpath(__file__))}/test-cfn-nag-suppression-list.json",
- NagType.CFN_NAG,
- )
-
- l1_construct = test_stack.test_key.node.default_child
- if l1_construct is not None:
- cfn_nag_suppression.visit(l1_construct)
- template = assertions.Template.from_stack(test_stack)
- template.has_resource(
- "AWS::KMS::Key",
- {
- "Metadata": {
- "cfn_nag": {
- "rules_to_suppress": [
- {"id": "test-cfn-id", "reason": "test-cfn-reason"}
- ]
- }
- }
- },
- )
- else:
- assert False
diff --git a/source/backstage/cdk/source/tests/infrastructure/stacks/__init__.py b/source/backstage/cdk/source/tests/infrastructure/stacks/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/backstage/cdk/source/tests/infrastructure/stacks/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/backstage/cdk/source/tests/infrastructure/stacks/test_env.py b/source/backstage/cdk/source/tests/infrastructure/stacks/test_env.py
deleted file mode 100644
index f4363e92..00000000
--- a/source/backstage/cdk/source/tests/infrastructure/stacks/test_env.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Third Party Libraries
-import aws_cdk
-
-# Connected Mobility Solution on AWS
-from ....infrastructure.stacks.env import BackstageEnvStack
-
-app = aws_cdk.App()
-stack = BackstageEnvStack(
- app,
- "test-stack",
- env=aws_cdk.Environment(
- account="test-account-id",
- region="us-west-2",
- ),
-)
-template = aws_cdk.assertions.Template.from_stack(stack)
-
-
-def test_ec2_securitygroup() -> None:
- template.has_resource("AWS::EC2::SecurityGroup", {})
- template.resource_count_is("AWS::EC2::SecurityGroup", 2)
-
-
-def test_ec2_securitygroupingress() -> None:
- template.has_resource("AWS::EC2::SecurityGroupIngress", {})
- template.resource_count_is("AWS::EC2::SecurityGroupIngress", 1)
-
-
-def test_rds_dbcluster() -> None:
- template.has_resource("AWS::RDS::DBCluster", {})
- template.resource_count_is("AWS::RDS::DBCluster", 1)
-
-
-def test_rds_dbsubnetgroup() -> None:
- template.has_resource("AWS::RDS::DBSubnetGroup", {})
- template.resource_count_is("AWS::RDS::DBSubnetGroup", 1)
-
-
-def test_secretsmanager_secret() -> None:
- template.has_resource("AWS::SecretsManager::Secret", {})
- template.resource_count_is("AWS::SecretsManager::Secret", 1)
-
-
-def test_secretsmanager_secrettargetattachment() -> None:
- template.has_resource("AWS::SecretsManager::SecretTargetAttachment", {})
- template.resource_count_is("AWS::SecretsManager::SecretTargetAttachment", 1)
-
-
-def test_security_groups_ingress_rules_are_empty() -> None:
- security_groups = template.find_resources("AWS::EC2::SecurityGroup")
- for security_group in security_groups.values():
- # assert that no ingress rules are configured
- # when ingress rules needs to be configured, assert here that it is minimal
- assert not security_group["Properties"].get("SecurityGroupIngress")
diff --git a/source/backstage/cdk/source/tests/infrastructure/stacks/test_stack.py b/source/backstage/cdk/source/tests/infrastructure/stacks/test_stack.py
deleted file mode 100644
index 35d117d2..00000000
--- a/source/backstage/cdk/source/tests/infrastructure/stacks/test_stack.py
+++ /dev/null
@@ -1,122 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Third Party Libraries
-import aws_cdk
-
-
-def test_cognito_userpool(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::Cognito::UserPool", {})
- template.resource_count_is("AWS::Cognito::UserPool", 1)
-
-
-def test_cognito_userpoolclient(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::Cognito::UserPoolClient", {})
- template.resource_count_is("AWS::Cognito::UserPoolClient", 1)
-
-
-def test_cognito_userpooldomain(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::Cognito::UserPoolDomain", {})
- template.resource_count_is("AWS::Cognito::UserPoolDomain", 1)
-
-
-def test_cognito_userpooluser(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::Cognito::UserPoolUser", {})
- template.resource_count_is("AWS::Cognito::UserPoolUser", 1)
-
-
-def test_ec2_securitygroup(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::EC2::SecurityGroup", {})
- template.resource_count_is("AWS::EC2::SecurityGroup", 2)
-
-
-def test_ec2_securitygroupingress(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::EC2::SecurityGroupIngress", {})
- template.resource_count_is("AWS::EC2::SecurityGroupIngress", 2)
-
-
-def test_ecs_cluster(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::ECS::Cluster", {})
- template.resource_count_is("AWS::ECS::Cluster", 1)
-
-
-def test_ecs_service(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::ECS::Service", {})
- template.resource_count_is("AWS::ECS::Service", 1)
-
-
-def test_ecs_taskdefinition(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::ECS::TaskDefinition", {})
- template.resource_count_is("AWS::ECS::TaskDefinition", 1)
-
-
-def test_elasticloadbalancingv2_listener(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::ElasticLoadBalancingV2::Listener", {})
- template.resource_count_is("AWS::ElasticLoadBalancingV2::Listener", 1)
-
-
-def test_elasticloadbalancingv2_listenerrule(
- template: aws_cdk.assertions.Template,
-) -> None:
- template.has_resource("AWS::ElasticLoadBalancingV2::ListenerRule", {})
- template.resource_count_is("AWS::ElasticLoadBalancingV2::ListenerRule", 1)
-
-
-def test_elasticloadbalancingv2_loadbalancer(
- template: aws_cdk.assertions.Template,
-) -> None:
- template.has_resource("AWS::ElasticLoadBalancingV2::LoadBalancer", {})
- template.resource_count_is("AWS::ElasticLoadBalancingV2::LoadBalancer", 1)
-
-
-def test_elasticloadbalancingv2_targetgroup(
- template: aws_cdk.assertions.Template,
-) -> None:
- template.has_resource("AWS::ElasticLoadBalancingV2::TargetGroup", {})
- template.resource_count_is("AWS::ElasticLoadBalancingV2::TargetGroup", 1)
-
-
-def test_iam_policy(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::IAM::Policy", {})
- template.resource_count_is("AWS::IAM::Policy", 4)
-
-
-def test_iam_role(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::IAM::Role", {})
- template.resource_count_is("AWS::IAM::Role", 4)
-
-
-def test_lambda_function(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::Lambda::Function", {})
- template.resource_count_is("AWS::Lambda::Function", 3)
-
-
-def test_logs_loggroup(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::Logs::LogGroup", {})
- template.resource_count_is("AWS::Logs::LogGroup", 1)
-
-
-def test_route53_recordset(template: aws_cdk.assertions.Template) -> None:
- template.has_resource("AWS::Route53::RecordSet", {})
- template.resource_count_is("AWS::Route53::RecordSet", 2)
-
-
-def test_custom_userpoolcloudfrontdomainname(
- template: aws_cdk.assertions.Template,
-) -> None:
- template.has_resource("Custom::UserPoolCloudFrontDomainName", {})
- template.resource_count_is("Custom::UserPoolCloudFrontDomainName", 1)
-
-
-def test_security_groups_ingress_rules_are_empty_or_valid(
- template: aws_cdk.assertions.Template,
-) -> None:
- allowed_tcp_ports = [80, 443]
- security_groups = template.find_resources("AWS::EC2::SecurityGroup")
- for security_group in security_groups.values():
- # when ingress rules needs to be configured, assert here that it is minimal
- ingress_rules = security_group["Properties"].get("SecurityGroupIngress", [])
- for ingress_rule in ingress_rules:
- assert ingress_rule["FromPort"] in allowed_tcp_ports
- assert ingress_rule["ToPort"] in allowed_tcp_ports
diff --git a/source/backstage/examples/template/template.yaml b/source/backstage/examples/template/template.yaml
deleted file mode 100644
index 55864505..00000000
--- a/source/backstage/examples/template/template.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: scaffolder.backstage.io/v1beta3
-# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-template
-kind: Template
-metadata:
- name: example-nodejs-template
- title: Example Node.js Template
- description: An example template for the scaffolder that creates a simple Node.js service
-spec:
- owner: user:guest
- type: service
-
- # These parameters are used to generate the input form in the frontend, and are
- # used to gather input data for the execution of the template.
- parameters:
- - title: Fill in some steps
- required:
- - name
- properties:
- name:
- title: Name
- type: string
- description: Unique name of the component
- ui:autofocus: true
- ui:options:
- rows: 5
-
- # These steps are executed in the scaffolder backend, using data that we gathered
- # via the parameters above.
- steps:
- # Each step executes an action, in this case one templates files into the working directory.
- - id: createProtonSpec
- name: Create Proton Service Spec
- action: aws:fs:write-yaml
- input:
- filename: spec.yaml
- entity:
- proton: ServiceSpec
- instances:
- - name: "dev"
- environment: "cms_environment"
- spec: {}
-
- # The final step is to register our new component in the catalog.
- - id: register
- name: Register
- action: catalog:register
- input:
- catalogInfoPath: '/catalog-info.yaml'
-
- # Outputs are displayed to the user after a successful execution of the template.
- output:
- links:
- - title: Open in catalog
- icon: catalog
- entityRef: ${{ steps['register'].output.entityRef }}
diff --git a/source/backstage/package.json b/source/backstage/package.json
deleted file mode 100644
index ee4c28f6..00000000
--- a/source/backstage/package.json
+++ /dev/null
@@ -1,63 +0,0 @@
-{
- "name": "cms-backstage",
- "version": "1.0.4",
- "private": true,
- "license": "Apache-2.0",
- "description": "Backstage implementation preconfigured to work with CMS",
- "engines": {
- "node": "18 || 20"
- },
- "scripts": {
- "dev": "concurrently \"yarn start\" \"yarn start-backend\"",
- "start": "yarn workspace app start",
- "start-backend": "yarn workspace backend start",
- "build:backend": "yarn workspace backend build",
- "build:all": "backstage-cli repo build --all",
- "build-image": "yarn workspace backend build-image",
- "tsc": "tsc",
- "tsc:full": "tsc --skipLibCheck false --incremental false",
- "clean": "backstage-cli repo clean",
- "test": "backstage-cli repo test",
- "test:all": "backstage-cli repo test --coverage",
- "lint": "backstage-cli repo lint --since origin/mainline",
- "lint:all": "backstage-cli repo lint",
- "prettier:check": "prettier --check .",
- "new": "backstage-cli new --scope internal"
- },
- "workspaces": {
- "packages": [
- "packages/*",
- "plugins/*"
- ]
- },
- "devDependencies": {
- "@backstage/cli": "^0.25.2",
- "@types/supertest": "^2.0.14",
- "concurrently": "^8.0.1",
- "lerna": "^7.1.5",
- "node-gyp": "^10.0.0",
- "prettier": "^3",
- "typescript": "^5.3.2",
- "xml2js": "^0.5.0",
- "yaml": "^2.2.2"
- },
- "resolutions": {
- "@types/react": "^18",
- "@types/react-dom": "^18",
- "@backstage/plugin-home": "^0.6.2",
- "@backstage/backend-app-api": "^0.5.13",
- "@backstage/backend-common": "^0.21.2",
- "@backstage/backend-plugin-api": "^0.6.12",
- "@backstage/core-components": "^0.14.0",
- "@backstage/theme": "^0.5.1"
- },
- "lint-staged": {
- "*.{js,jsx,ts,tsx,mjs,cjs}": [
- "eslint --fix",
- "prettier --write"
- ],
- "*.{json,md}": [
- "prettier --write"
- ]
- }
-}
diff --git a/source/backstage/packages/app/package.json b/source/backstage/packages/app/package.json
deleted file mode 100644
index 6fe33943..00000000
--- a/source/backstage/packages/app/package.json
+++ /dev/null
@@ -1,102 +0,0 @@
-{
- "name": "app",
- "version": "1.0.4",
- "private": true,
- "bundled": true,
- "license": "Apache-2.0",
- "description": "Backstage frontend package",
- "backstage": {
- "role": "frontend"
- },
- "scripts": {
- "start": "backstage-cli package start",
- "build": "backstage-cli package build",
- "clean": "backstage-cli package clean",
- "test": "backstage-cli package test --coverage --silent",
- "lint": "backstage-cli package lint",
- "test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
- "test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
- "cy:dev": "cypress open",
- "cy:run": "cypress run --browser chrome"
- },
- "dependencies": {
- "@aws/aws-codeservices-plugin-for-backstage": "0.1.3",
- "@aws/aws-proton-plugin-for-backstage": "0.2.2",
- "@backstage/app-defaults": "^1.5.0",
- "@backstage/catalog-model": "^1.4.4",
- "@backstage/cli": "^0.25.2",
- "@backstage/core-app-api": "^1.12.0",
- "@backstage/core-components": "^0.14.0",
- "@backstage/core-plugin-api": "^1.9.0",
- "@backstage/integration-react": "^1.1.24",
- "@backstage/plugin-api-docs": "^0.11.0",
- "@backstage/plugin-catalog": "^1.17.0",
- "@backstage/plugin-catalog-common": "^1.0.21",
- "@backstage/plugin-catalog-graph": "^0.4.0",
- "@backstage/plugin-catalog-import": "^0.10.6",
- "@backstage/plugin-catalog-react": "^1.10.0",
- "@backstage/plugin-github-actions": "^0.6.11",
- "@backstage/plugin-home": "^0.6.2",
- "@backstage/plugin-org": "^0.6.20",
- "@backstage/plugin-permission-react": "^0.4.20",
- "@backstage/plugin-scaffolder": "^1.18.0",
- "@backstage/plugin-search": "^1.4.6",
- "@backstage/plugin-search-react": "^1.7.6",
- "@backstage/plugin-tech-radar": "^0.6.13",
- "@backstage/plugin-techdocs": "^1.10.0",
- "@backstage/plugin-techdocs-module-addons-contrib": "^1.1.5",
- "@backstage/plugin-techdocs-react": "^1.1.16",
- "@backstage/plugin-user-settings": "^0.8.1",
- "@backstage/theme": "^0.5.1",
- "@gitbeaker/rest": "39.10.3",
- "@immobiliarelabs/backstage-plugin-gitlab": "6.0.0",
- "@roadiehq/backstage-plugin-home-rss": "1.2.11",
- "@react-hookz/web": "^23.1.0",
- "react": "^18",
- "react-dom": "^18",
- "react-router": "^6.3.0",
- "react-router-dom": "^6.3.0",
- "sanitize-html": "2.10.0"
- },
- "devDependencies": {
- "@backstage/test-utils": "^1.5.0",
- "@testing-library/dom": "^9.0.0",
- "@testing-library/jest-dom": "^6.0.0",
- "@testing-library/react": "^14.0.0",
- "@testing-library/user-event": "^14.0.0",
- "@types/node": "20.1.1",
- "@types/react": "*",
- "@types/react-dom": "*",
- "@types/react-router": "*",
- "@types/react-router-dom": "*",
- "@types/sanitize-html": "^2.9.0",
- "cross-env": "7.0.3",
- "cypress": "^13.3.0",
- "eslint": "^8",
- "eslint-plugin-cypress": "^2",
- "jsonwebtoken": "9.0.0",
- "start-server-and-test": "2.0.0"
- },
- "browserslist": {
- "production": [
- ">0.2%",
- "not dead",
- "not op_mini all"
- ],
- "development": [
- "last 1 chrome version",
- "last 1 firefox version",
- "last 1 safari version"
- ]
- },
- "files": [
- "dist"
- ],
- "jest": {
- "coverageThreshold": {
- "global": {
- "lines": 80
- }
- }
- }
-}
diff --git a/source/backstage/packages/app/src/App.tsx b/source/backstage/packages/app/src/App.tsx
deleted file mode 100644
index 62411caf..00000000
--- a/source/backstage/packages/app/src/App.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import React from 'react';
-import { Route } from 'react-router-dom';
-import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
-import {
- CatalogEntityPage,
- CatalogIndexPage,
- catalogPlugin,
-} from '@backstage/plugin-catalog';
-import {
- CatalogImportPage,
- catalogImportPlugin,
-} from '@backstage/plugin-catalog-import';
-import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
-import { orgPlugin } from '@backstage/plugin-org';
-import { SearchPage } from '@backstage/plugin-search';
-import { TechRadarPage } from '@backstage/plugin-tech-radar';
-import {
- TechDocsIndexPage,
- techdocsPlugin,
- TechDocsReaderPage,
-} from '@backstage/plugin-techdocs';
-import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
-import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
-import { UserSettingsPage } from '@backstage/plugin-user-settings';
-import { HomepageCompositionRoot } from '@backstage/plugin-home';
-import { apis } from './apis';
-import { entityPage } from './components/catalog/EntityPage';
-import { searchPage } from './components/search/SearchPage';
-import { Root } from './components/Root';
-
-import {
- AlertDisplay,
- OAuthRequestDialog,
- SignInPage,
-} from '@backstage/core-components';
-import { createApp } from '@backstage/app-defaults';
-import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
-import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
-import { RequirePermission } from '@backstage/plugin-permission-react';
-import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
-
-import { cognitoAuthApiRef } from './custom/AwsCognitoAuth';
-import {
- discoveryApiRef,
- useApi,
- IdentityApi,
-} from '@backstage/core-plugin-api';
-import { setTokenCookie } from './custom/CookieAuth';
-
-import { HomePage } from './components/home/HomePage';
-
-const app = createApp({
- apis,
- components: {
- SignInPage: props => {
- const discoveryApi = useApi(discoveryApiRef);
- return (
- {
- setTokenCookie(
- await discoveryApi.getBaseUrl('cookie'),
- identityApi,
- );
- props.onSignInSuccess(identityApi);
- }}
- />
- );
- },
- },
- bindRoutes({ bind }) {
- bind(catalogPlugin.externalRoutes, {
- createComponent: scaffolderPlugin.routes.root,
- viewTechDoc: techdocsPlugin.routes.docRoot,
- });
- bind(apiDocsPlugin.externalRoutes, {
- registerApi: catalogImportPlugin.routes.importPage,
- });
- bind(scaffolderPlugin.externalRoutes, {
- registerComponent: catalogImportPlugin.routes.importPage,
- });
- bind(orgPlugin.externalRoutes, {
- catalogIndex: catalogPlugin.routes.catalogIndex,
- });
- },
-});
-
-const routes = (
-
- }>
-
-
- } />
- }
- >
- {entityPage}
-
- } />
- }
- >
-
-
-
-
- } />
- } />
- }
- />
-
-
-
- }
- />
- }>
- {searchPage}
-
- } />
- } />
-
-);
-
-export default app.createRoot(
- <>
-
-
-
- {routes}
-
- >,
-);
diff --git a/source/backstage/packages/app/src/__tests__/App.test.tsx b/source/backstage/packages/app/src/__tests__/App.test.tsx
deleted file mode 100644
index f8abee72..00000000
--- a/source/backstage/packages/app/src/__tests__/App.test.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import React from 'react';
-import { renderWithEffects } from '@backstage/test-utils';
-import App from '../App';
-
-beforeAll(() => {
- window.open = jest.fn();
-});
-
-describe('App', () => {
- it('should render', async () => {
- process.env = {
- NODE_ENV: 'test',
- APP_CONFIG: [
- {
- data: {
- app: { title: 'Test' },
- backend: { baseUrl: 'http://localhost:7007' },
- techdocs: {
- storageUrl: 'http://localhost:7007/api/techdocs/static/docs',
- },
- },
- context: 'test',
- },
- ] as any,
- };
-
- const rendered = await renderWithEffects();
- expect(rendered.baseElement).toBeInTheDocument();
- });
-});
diff --git a/source/backstage/packages/app/src/apis.ts b/source/backstage/packages/app/src/apis.ts
deleted file mode 100644
index 155c582d..00000000
--- a/source/backstage/packages/app/src/apis.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import {
- ScmIntegrationsApi,
- scmIntegrationsApiRef,
- ScmAuth,
-} from '@backstage/integration-react';
-import {
- AnyApiFactory,
- configApiRef,
- createApiFactory,
- discoveryApiRef,
- oauthRequestApiRef,
-} from '@backstage/core-plugin-api';
-
-import { cognitoAuthApiRef } from './custom/AwsCognitoAuth';
-import { OAuth2 } from '@backstage/core-app-api';
-import { UserIcon } from '@backstage/core-components';
-
-export const apis: AnyApiFactory[] = [
- createApiFactory({
- api: cognitoAuthApiRef,
- deps: {
- discoveryApi: discoveryApiRef,
- oauthRequestApi: oauthRequestApiRef,
- configApi: configApiRef,
- },
- factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
- OAuth2.create({
- discoveryApi,
- oauthRequestApi,
- environment: configApi.getOptionalString('auth.environment'),
- provider: {
- id: 'cognito',
- title: 'AWS Cognito',
- icon: UserIcon,
- },
- }),
- }),
- createApiFactory({
- api: scmIntegrationsApiRef,
- deps: { configApi: configApiRef },
- factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
- }),
- ScmAuth.createDefaultApiFactory(),
-];
diff --git a/source/backstage/packages/app/src/components/Root/Root.tsx b/source/backstage/packages/app/src/components/Root/Root.tsx
deleted file mode 100644
index 8c1af863..00000000
--- a/source/backstage/packages/app/src/components/Root/Root.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import React, { PropsWithChildren } from 'react';
-import { makeStyles } from '@material-ui/core';
-import HomeIcon from '@material-ui/icons/Home';
-import ExtensionIcon from '@material-ui/icons/Extension';
-import MapIcon from '@material-ui/icons/MyLocation';
-import CategoryIcon from '@material-ui/icons/Category';
-import LibraryBooks from '@material-ui/icons/LibraryBooks';
-import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
-import LogoFull from './LogoFull';
-import LogoIcon from './LogoIcon';
-import {
- Settings as SidebarSettings,
- UserSettingsSignInAvatar,
-} from '@backstage/plugin-user-settings';
-import { SidebarSearchModal } from '@backstage/plugin-search';
-import {
- Sidebar,
- sidebarConfig,
- SidebarDivider,
- SidebarGroup,
- SidebarItem,
- SidebarPage,
- SidebarScrollWrapper,
- SidebarSpace,
- useSidebarOpenState,
- Link,
-} from '@backstage/core-components';
-import MenuIcon from '@material-ui/icons/Menu';
-import SearchIcon from '@material-ui/icons/Search';
-
-const useSidebarLogoStyles = makeStyles({
- root: {
- width: sidebarConfig.drawerWidthClosed,
- height: 3 * sidebarConfig.logoHeight,
- display: 'flex',
- flexFlow: 'row nowrap',
- alignItems: 'center',
- marginBottom: -14,
- },
- link: {
- width: sidebarConfig.drawerWidthClosed,
- marginLeft: 24,
- },
-});
-
-const SidebarLogo = () => {
- const classes = useSidebarLogoStyles();
- const { isOpen } = useSidebarOpenState();
-
- return (
-
-
- {isOpen ? : }
-
-
- );
-};
-
-export const Root = ({ children }: PropsWithChildren<{}>) => (
-
-
-
- } to="/search">
-
-
-
- }>
- {/* Global nav, not org-specific */}
-
-
-
-
-
- {/* End global nav */}
-
-
-
-
-
-
-
- }
- to="/settings"
- >
-
-
-
- {children}
-
-);
diff --git a/source/backstage/packages/app/src/components/Root/index.ts b/source/backstage/packages/app/src/components/Root/index.ts
deleted file mode 100644
index 95919477..00000000
--- a/source/backstage/packages/app/src/components/Root/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-export { Root } from './Root';
diff --git a/source/backstage/packages/app/src/components/catalog/EntityPage.tsx b/source/backstage/packages/app/src/components/catalog/EntityPage.tsx
deleted file mode 100644
index 46c4d171..00000000
--- a/source/backstage/packages/app/src/components/catalog/EntityPage.tsx
+++ /dev/null
@@ -1,431 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import React from 'react';
-import { Button, Grid } from '@material-ui/core';
-import {
- EntityApiDefinitionCard,
- EntityConsumedApisCard,
- EntityConsumingComponentsCard,
- EntityHasApisCard,
- EntityProvidedApisCard,
- EntityProvidingComponentsCard,
-} from '@backstage/plugin-api-docs';
-import {
- EntityAboutCard,
- EntityDependsOnComponentsCard,
- EntityDependsOnResourcesCard,
- EntityHasComponentsCard,
- EntityHasResourcesCard,
- EntityHasSubcomponentsCard,
- EntityHasSystemsCard,
- EntityLayout,
- EntityLinksCard,
- EntitySwitch,
- EntityOrphanWarning,
- EntityProcessingErrorsPanel,
- isComponentType,
- isKind,
- hasCatalogProcessingErrors,
- isOrphan,
-} from '@backstage/plugin-catalog';
-import {
- isGithubActionsAvailable,
- EntityGithubActionsContent,
-} from '@backstage/plugin-github-actions';
-import {
- EntityUserProfileCard,
- EntityGroupProfileCard,
- EntityMembersListCard,
- EntityOwnershipCard,
-} from '@backstage/plugin-org';
-import { EntityTechdocsContent } from '@backstage/plugin-techdocs';
-import { EmptyState } from '@backstage/core-components';
-import {
- Direction,
- EntityCatalogGraphCard,
-} from '@backstage/plugin-catalog-graph';
-import {
- RELATION_API_CONSUMED_BY,
- RELATION_API_PROVIDED_BY,
- RELATION_CONSUMES_API,
- RELATION_DEPENDENCY_OF,
- RELATION_DEPENDS_ON,
- RELATION_HAS_PART,
- RELATION_PART_OF,
- RELATION_PROVIDES_API,
-} from '@backstage/catalog-model';
-
-import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
-import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
-import {
- isGitlabAvailable,
- EntityGitlabContent,
- EntityGitlabLanguageCard,
- EntityGitlabPeopleCard,
- EntityGitlabMergeRequestsTable,
- EntityGitlabMergeRequestStatsCard,
- EntityGitlabPipelinesTable,
-} from '@immobiliarelabs/backstage-plugin-gitlab';
-
-import {
- EntityAWSCodePipelineContent,
- EntityAWSCodePipelineOverviewCard,
- EntityAWSCodeBuildProjectOverviewCard,
- isAWSCodePipelineAvailable,
- isAWSCodeBuildProjectAvailable,
-} from '@aws/aws-codeservices-plugin-for-backstage';
-import {
- EntityAWSProtonServiceOverviewCard,
- isAWSProtonServiceAvailable,
-} from '@aws/aws-proton-plugin-for-backstage';
-
-const techdocsContent = (
-
-
-
-
-
-);
-
-const cicdContent = (
- // This is an example of how you can implement your company's logic in entity page.
- // You can for example enforce that all components of type 'service' should use GitHubActions
-
-
-
-
-
-
-
-
-
- Read more
-
- }
- />
-
-
-);
-
-const entityWarningContent = (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
-);
-
-const overviewContent = (
-
- {entityWarningContent}
-
-
-
-
- Boolean(isAWSProtonServiceAvailable(e))}>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-const serviceEntityPage = (
-
-
- {overviewContent}
-
-
-
- {cicdContent}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {techdocsContent}
-
-
-);
-
-const websiteEntityPage = (
-
-
- {overviewContent}
-
-
-
- {cicdContent}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {techdocsContent}
-
-
-);
-
-/**
- * NOTE: This page is designed to work on small screens such as mobile devices.
- * This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`,
- * since this does not default. If no breakpoints are used, the items will equitably share the available space.
- * https://material-ui.com/components/grid/#basic-grid.
- */
-
-const defaultEntityPage = (
-
-
- {overviewContent}
-
-
-
- {techdocsContent}
-
-
-);
-
-const componentPage = (
-
-
- {serviceEntityPage}
-
-
-
- {websiteEntityPage}
-
-
- {defaultEntityPage}
-
-);
-
-const apiPage = (
-
-
-
- {entityWarningContent}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-const userPage = (
-
-
-
- {entityWarningContent}
-
-
-
-
-
-
-
-
-
-);
-
-const groupPage = (
-
-
-
- {entityWarningContent}
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-const systemPage = (
-
-
-
- {entityWarningContent}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-const domainPage = (
-
-
-
- {entityWarningContent}
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-export const entityPage = (
-
-
-
-
-
-
-
-
- {defaultEntityPage}
-
-);
diff --git a/source/backstage/packages/app/src/components/home/HomePage.tsx b/source/backstage/packages/app/src/components/home/HomePage.tsx
deleted file mode 100644
index 4efd2984..00000000
--- a/source/backstage/packages/app/src/components/home/HomePage.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { Page, Header, Content } from '@backstage/core-components';
-import {
- ClockConfig,
- HeaderWorldClock,
- HomePageStarredEntities,
-} from '@backstage/plugin-home';
-import { HomePageSearchBar } from '@backstage/plugin-search';
-import { Grid, makeStyles } from '@material-ui/core';
-import { useUserProfile } from '@backstage/plugin-user-settings';
-import React from 'react';
-
-export const HomePage = () => {
- const clockConfigs: ClockConfig[] = [
- {
- label: 'East Coast',
- timeZone: 'America/New_York',
- },
- {
- label: 'Central',
- timeZone: 'America/Chicago',
- },
- {
- label: 'Mountain',
- timeZone: 'America/Denver',
- },
- {
- label: 'Pacific',
- timeZone: 'America/Los_Angeles',
- },
- ];
-
- const timeFormat: Intl.DateTimeFormatOptions = {
- hour: '2-digit',
- minute: '2-digit',
- hour12: true,
- };
-
- const userProfile = useUserProfile();
-
- const useStyles = makeStyles(theme => ({
- searchBar: {
- display: 'flex',
- maxWidth: '60vw',
- backgroundColor: theme.palette.background.paper,
- boxShadow: theme.shadows[1],
- borderRadius: '50px',
- margin: 'auto',
- },
- }));
-
- const classes = useStyles();
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/source/backstage/packages/app/src/components/home/RSSFeed/__tests__/rssApi.test.tsx b/source/backstage/packages/app/src/components/home/RSSFeed/__tests__/rssApi.test.tsx
deleted file mode 100644
index 0a9c2e80..00000000
--- a/source/backstage/packages/app/src/components/home/RSSFeed/__tests__/rssApi.test.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { useRssFeed } from '../rssApi';
-import { screen, waitFor } from '@testing-library/react';
-import { renderWithEffects } from '@backstage/test-utils';
-import React from 'react';
-
-jest.mock('@backstage/core-plugin-api', () => ({
- ...jest.requireActual('@backstage/core-plugin-api'),
- useApi: jest.fn().mockReturnValue({
- getBaseUrl: jest.fn().mockReturnValue('http://localhost:3000'),
- getCredentials: jest.fn().mockReturnValue('test-token'),
- }),
-}));
-
-beforeAll(() => {
- const mockFeedResponse = {
- ok: true,
- text: jest
- .fn()
- .mockResolvedValue(
- '' +
- '' +
- '- ' +
- 'test-1' +
- '
' +
- '- ' +
- 'test-2' +
- '
' +
- '',
- ),
- };
-
- global.fetch = jest.fn().mockReturnValue(mockFeedResponse);
-});
-
-describe('rssApi', () => {
- it('useRssFeed loads feed as expected', async () => {
- const MockComponentWithRssFeed = () => {
- const { data } = useRssFeed('test');
- return (
-
- {data?.map((row, i) => (
-
{row.title}
- ))}
-
- );
- };
- await renderWithEffects();
- await waitFor(() => {
- expect(screen.getByText('test-1')).toBeInTheDocument();
- expect(screen.getByText('test-2')).toBeInTheDocument();
- });
- });
-});
diff --git a/source/backstage/packages/app/src/components/home/RSSFeed/index.tsx b/source/backstage/packages/app/src/components/home/RSSFeed/index.tsx
deleted file mode 100644
index 229a1d58..00000000
--- a/source/backstage/packages/app/src/components/home/RSSFeed/index.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { ItemCardGrid, ItemCardHeader, Link } from '@backstage/core-components';
-import { Grid, Typography } from '@material-ui/core';
-import Card from '@material-ui/core/Card';
-import CardActions from '@material-ui/core/CardActions';
-import CardContent from '@material-ui/core/CardContent';
-import CardMedia from '@material-ui/core/CardMedia';
-import React from 'react';
-import { useRssFeed } from './rssApi';
-import { Sanitized } from './sanitized';
-
-export interface RSSFeedProps {
- rssSource: string;
- title: string;
-}
-
-export const RSSFeed = ({ rssSource, title }: RSSFeedProps) => {
- const { data } = useRssFeed(rssSource);
- return (
-
-
- {title}
-
-
-
- {data &&
- data.map(item => (
-
-
-
-
-
-
-
-
- Go to the article...
-
-
- ))}
-
-
-
- );
-};
diff --git a/source/backstage/packages/app/src/components/home/RSSFeed/rssApi.ts b/source/backstage/packages/app/src/components/home/RSSFeed/rssApi.ts
deleted file mode 100644
index 5a2ac7a0..00000000
--- a/source/backstage/packages/app/src/components/home/RSSFeed/rssApi.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { useEffect, useState } from 'react';
-import {
- useApi,
- identityApiRef,
- discoveryApiRef,
-} from '@backstage/core-plugin-api';
-
-export type AtomItem = {
- title: string;
- pubDate?: string;
- published?: string;
- description: string;
- content: string;
- link: string;
-};
-
-const parseItem = (item: Element): AtomItem => {
- const itemData: { [key: string]: any } = {};
- item.childNodes.forEach(node => {
- itemData[node.nodeName] = node.textContent;
- });
- return itemData as AtomItem;
-};
-
-export const useRssFeed = (feed?: string) => {
- const [status, setStatus] = useState<{
- loading: boolean;
- error?: Error;
- data?: AtomItem[];
- }>({
- loading: false,
- });
-
- const identityApi = useApi(identityApiRef);
- const discoveryApi = useApi(discoveryApiRef);
-
- const loadFeed = async (feed: string) => {
- setStatus({ loading: true });
- const feedUrl = `${await discoveryApi.getBaseUrl('proxy')}/rss/${feed}`;
- const authToken = `Bearer ${await (
- await identityApi.getCredentials()
- ).token}`;
- const response = await fetch(feedUrl, {
- headers: {
- Authorization: authToken,
- },
- });
-
- if (!response.ok) {
- setStatus({
- loading: false,
- error: new Error(
- `Failed to fetch feed ${feed}: ${response.statusText}`,
- ),
- });
- } else {
- const rssTextData = await response.text();
- const rssData = new DOMParser().parseFromString(rssTextData, 'text/xml');
- const items = Array.from(rssData.querySelectorAll('item,entry')).map(
- parseItem,
- );
- setStatus({ loading: false, data: items });
- }
- };
-
- useEffect(() => {
- if (feed) {
- loadFeed(feed);
- }
- }, []);
-
- return { ...status, loadFeed };
-};
diff --git a/source/backstage/packages/app/src/components/home/RSSFeed/sanitized.tsx b/source/backstage/packages/app/src/components/home/RSSFeed/sanitized.tsx
deleted file mode 100644
index b853cb79..00000000
--- a/source/backstage/packages/app/src/components/home/RSSFeed/sanitized.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import sanitizeHtml from 'sanitize-html';
-import React from 'react';
-
-const defaultOptions = {
- allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p'],
- allowedAttributes: {
- a: ['href'],
- },
- allowedIframeHostnames: [],
-};
-
-const sanitize = (dirty: string, options: any) => ({
- __html: sanitizeHtml(
- dirty,
- { ...defaultOptions, ...options } || defaultOptions,
- ),
-});
-
-export interface SanitizedProps {
- text: string;
-}
-
-export const Sanitized = ({ text }: SanitizedProps) => (
-
-);
diff --git a/source/backstage/packages/app/src/components/home/icons/GitLab.tsx b/source/backstage/packages/app/src/components/home/icons/GitLab.tsx
deleted file mode 100644
index bf417c43..00000000
--- a/source/backstage/packages/app/src/components/home/icons/GitLab.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import React from 'react';
-import { makeStyles } from '@material-ui/core';
-
-const useStyles = makeStyles({
- svg: {
- width: 'auto',
- height: 28,
- },
- path: {
- fill: '#7df3e1',
- },
-});
-
-const GitlabIcon = () => {
- const classes = useStyles();
-
- return (
-
- );
-};
-
-export default GitlabIcon;
diff --git a/source/backstage/packages/app/src/components/search/SearchPage.tsx b/source/backstage/packages/app/src/components/search/SearchPage.tsx
deleted file mode 100644
index 6bcd926b..00000000
--- a/source/backstage/packages/app/src/components/search/SearchPage.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import React from 'react';
-import { makeStyles, Theme, Grid, Paper } from '@material-ui/core';
-
-import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
-import {
- catalogApiRef,
- CATALOG_FILTER_EXISTS,
-} from '@backstage/plugin-catalog-react';
-import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
-
-import { SearchType } from '@backstage/plugin-search';
-import {
- SearchBar,
- SearchFilter,
- SearchResult,
- SearchPagination,
- useSearch,
-} from '@backstage/plugin-search-react';
-import {
- CatalogIcon,
- Content,
- DocsIcon,
- Header,
- Page,
-} from '@backstage/core-components';
-import { useApi } from '@backstage/core-plugin-api';
-
-const useStyles = makeStyles((theme: Theme) => ({
- bar: {
- padding: theme.spacing(1, 0),
- },
- filters: {
- padding: theme.spacing(2),
- marginTop: theme.spacing(2),
- },
- filter: {
- '& + &': {
- marginTop: theme.spacing(2.5),
- },
- },
-}));
-
-const SearchPage = () => {
- const classes = useStyles();
- const { types } = useSearch();
- const catalogApi = useApi(catalogApiRef);
-
- return (
-
-
-
-
-
-
-
-
-
-
- ,
- },
- {
- value: 'techdocs',
- name: 'Documentation',
- icon: ,
- },
- ]}
- />
-
- {types.includes('techdocs') && (
- {
- // Return a list of entities which are documented.
- const { items } = await catalogApi.getEntities({
- fields: ['metadata.name'],
- filter: {
- 'metadata.annotations.backstage.io/techdocs-ref':
- CATALOG_FILTER_EXISTS,
- },
- });
-
- const names = items.map(entity => entity.metadata.name);
- names.sort();
- return names;
- }}
- />
- )}
-
-
-
-
-
-
-
- } />
- } />
-
-
-
-
-
- );
-};
-
-export const searchPage = ;
diff --git a/source/backstage/packages/app/src/custom/__tests__/CookieAuth.test.ts b/source/backstage/packages/app/src/custom/__tests__/CookieAuth.test.ts
deleted file mode 100644
index 3670e2ff..00000000
--- a/source/backstage/packages/app/src/custom/__tests__/CookieAuth.test.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { setTokenCookie } from '../CookieAuth';
-import type { IdentityApi } from '@backstage/core-plugin-api';
-import jwt from 'jsonwebtoken';
-
-beforeAll(() => {
- global.fetch = jest.fn();
- jest.useFakeTimers();
-});
-
-afterEach(() => {
- jest.resetAllMocks();
-});
-
-describe('CookieAuth', () => {
- it('Should call endpoint to set token cookie', async () => {
- const mockJwt = jwt.sign({ test: 'test' }, 'test', { expiresIn: '1h' });
- const mockIdentityApi: IdentityApi = {
- getBackstageIdentity: jest.fn(),
- getCredentials: jest.fn().mockReturnValue({ token: mockJwt }),
- getProfileInfo: jest.fn(),
- signOut: jest.fn(),
- };
-
- await setTokenCookie('https://localhost:3000', mockIdentityApi);
- expect(mockIdentityApi.getCredentials).toBeCalledTimes(1);
- jest.runOnlyPendingTimers();
- expect(mockIdentityApi.getCredentials).toBeCalledTimes(2);
- });
-
- it('Should not call endpoint to set token cookie if token is null', async () => {
- const mockIdentityApi: IdentityApi = {
- getBackstageIdentity: jest.fn(),
- getCredentials: jest.fn().mockReturnValue({ token: null }),
- getProfileInfo: jest.fn(),
- signOut: jest.fn(),
- };
- await setTokenCookie('https://localhost:3000', mockIdentityApi);
- expect(mockIdentityApi.getCredentials).toBeCalled();
- expect(global.fetch).not.toBeCalled();
- });
-});
diff --git a/source/backstage/packages/app/src/index.tsx b/source/backstage/packages/app/src/index.tsx
deleted file mode 100644
index 36b2f651..00000000
--- a/source/backstage/packages/app/src/index.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import '@backstage/cli/asset-types';
-import React from 'react';
-import ReactDOM from 'react-dom';
-import App from './App';
-
-ReactDOM.render(, document.getElementById('root'));
diff --git a/source/backstage/packages/app/src/setupTests.ts b/source/backstage/packages/app/src/setupTests.ts
deleted file mode 100644
index 13cd7197..00000000
--- a/source/backstage/packages/app/src/setupTests.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import '@testing-library/jest-dom';
diff --git a/source/backstage/packages/backend/package.json b/source/backstage/packages/backend/package.json
deleted file mode 100644
index abe762d7..00000000
--- a/source/backstage/packages/backend/package.json
+++ /dev/null
@@ -1,70 +0,0 @@
-{
- "name": "backend",
- "version": "1.0.0",
- "main": "dist/index.cjs.js",
- "types": "src/index.ts",
- "private": true,
- "license": "Apache-2.0",
- "description": "Backstage backend package",
- "backstage": {
- "role": "backend"
- },
- "scripts": {
- "start": "backstage-cli package start",
- "build": "backstage-cli package build",
- "lint": "backstage-cli package lint",
- "test": "backstage-cli package test --coverage --silent",
- "clean": "backstage-cli package clean",
- "build-image": "docker build ../.. -f Dockerfile --tag backstage"
- },
- "dependencies": {
- "@aws-crypto/sha256-js": "5.0.0",
- "@aws-sdk/client-eks": "3.515.0",
- "@aws-sdk/client-secrets-manager": "3.515.0",
- "@aws-sdk/client-sts": "3.515.0",
- "@aws-sdk/credential-providers": "3.427.0",
- "@aws-sdk/signature-v4": "3.374.0",
- "@aws-sdk/client-cognito-identity-provider": "3.515.0",
- "@aws/aws-codeservices-backend-plugin-for-backstage": "0.1.3",
- "@aws/aws-proton-backend-plugin-for-backstage": "0.2.2",
- "@backstage/backend-common": "^0.21.2",
- "@backstage/backend-tasks": "^0.5.17",
- "@backstage/catalog-client": "^1.6.0",
- "@backstage/catalog-model": "^1.4.4",
- "@backstage/config": "^1.1.1",
- "@backstage/plugin-app-backend": "^0.3.60",
- "@backstage/plugin-auth-backend": "^0.21.2",
- "@backstage/plugin-auth-node": "^0.4.7",
- "@backstage/plugin-catalog-backend": "^1.17.2",
- "@backstage/plugin-catalog-backend-module-aws": "^0.3.6",
- "@backstage/plugin-catalog-backend-module-gitlab": "^0.3.9",
- "@backstage/plugin-code-coverage-backend": "^0.2.26",
- "@backstage/plugin-events-backend": "^0.2.21",
- "@backstage/plugin-events-backend-module-gitlab": "^0.1.22",
- "@backstage/plugin-permission-common": "^0.7.12",
- "@backstage/plugin-permission-node": "^0.7.23",
- "@backstage/plugin-proxy-backend": "^0.4.10",
- "@backstage/plugin-scaffolder-backend": "^1.21.2",
- "@backstage/plugin-scaffolder-backend-module-gitlab": "^0.2.15",
- "@backstage/plugin-search-backend": "^1.5.2",
- "@backstage/plugin-search-backend-module-pg": "^0.5.21",
- "@backstage/plugin-search-backend-node": "^1.2.16",
- "@backstage/plugin-techdocs-backend": "^1.9.5",
- "@immobiliarelabs/backstage-plugin-gitlab-backend": "6.0.0",
- "app": "file:../app",
- "prettier": "^3",
- "jwt-decode": "^3.1.0"
- },
- "devDependencies": {
- "@backstage/cli": "^0.25.2",
- "@types/cookie-parser": "1.4.3",
- "@types/dockerode": "3.3.17",
- "@types/luxon": "3.3.0",
- "@types/passport-oauth2": "1.4.12",
- "@types/uuid": "^9.0.2",
- "supertest": "^6.3.3"
- },
- "files": [
- "dist"
- ]
-}
diff --git a/source/backstage/packages/backend/src/alb-auth/middleware.ts b/source/backstage/packages/backend/src/alb-auth/middleware.ts
deleted file mode 100644
index 3bfbcae1..00000000
--- a/source/backstage/packages/backend/src/alb-auth/middleware.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import type { Config } from '@backstage/config';
-import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
-import { NextFunction, Request, Response, RequestHandler } from 'express';
-import { PluginEnvironment } from '../types';
-import { decodeJwt } from 'jose';
-
-function setTokenCookie(
- res: Response,
- options: { token: string; secure: boolean; cookieDomain: string },
-) {
- try {
- const payload = decodeJwt(options.token);
- res.cookie('token', options.token, {
- expires: new Date(payload.exp ? payload.exp * 1000 : 0),
- secure: options.secure,
- sameSite: 'lax',
- domain: options.cookieDomain,
- path: '/',
- httpOnly: true,
- });
- } catch (_err) {
- // Ignore
- }
-}
-
-export const createAuthMiddleware = async (
- config: Config,
- appEnv: PluginEnvironment,
-) => {
- const authMiddleware: RequestHandler = async (
- req: Request,
- res: Response,
- next: NextFunction,
- ) => {
- try {
- appEnv.logger.debug(`ALB Headers [${JSON.stringify(req.headers)}]`);
- const token =
- getBearerTokenFromAuthorizationHeader(req.headers.authorization) ||
- (req.cookies?.token as string | undefined) ||
- (req.headers['x-amzn-oidc-data'] as string | undefined);
-
- if (!token) {
- throw new Error('Missing auth token');
- }
- if (!req.headers.authorization) {
- // getIdentity only seems to work off this header, coalesce all token options to this
- req.headers.authorization = `Bearer ${token}`;
- }
-
- req.user = await appEnv.identity.getIdentity({ request: req });
-
- if (!req.user) {
- throw new Error('getIdentity failed to set user');
- }
-
- appEnv.logger.debug(`Successfully authenticated`);
-
- if (token && token !== req.cookies?.token) {
- const baseUrl = config.getString('backend.baseUrl');
- const secure = baseUrl.startsWith('https://');
- const cookieDomain = new URL(baseUrl).hostname;
-
- setTokenCookie(res, {
- token,
- secure,
- cookieDomain,
- });
- }
-
- next();
- } catch (error) {
- appEnv.logger.debug(`Failed to authenticate: ${error}`, error);
- res.status(401).send('Unauthorized');
- }
- };
- return authMiddleware;
-};
diff --git a/source/backstage/packages/backend/src/index.test.ts b/source/backstage/packages/backend/src/index.test.ts
deleted file mode 100644
index 8c1daed3..00000000
--- a/source/backstage/packages/backend/src/index.test.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { PluginEnvironment } from './types';
-
-describe('test', () => {
- it('unbreaks the test runner', () => {
- const unbreaker = {} as PluginEnvironment;
- expect(unbreaker).toBeTruthy();
- });
-});
diff --git a/source/backstage/packages/backend/src/index.ts b/source/backstage/packages/backend/src/index.ts
deleted file mode 100644
index 7795055b..00000000
--- a/source/backstage/packages/backend/src/index.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import Router from 'express-promise-router';
-import {
- createServiceBuilder,
- loadBackendConfig,
- getRootLogger,
- useHotMemoize,
- notFoundHandler,
- CacheManager,
- DatabaseManager,
- SingleHostDiscovery,
- UrlReaders,
- ServerTokenManager,
-} from '@backstage/backend-common';
-import { TaskScheduler } from '@backstage/backend-tasks';
-import { Config } from '@backstage/config';
-import app from './plugins/app';
-import auth from './plugins/auth';
-import catalog from './plugins/catalog';
-import scaffolder from './plugins/scaffolder';
-import proxy from './plugins/proxy';
-import techdocs from './plugins/techdocs';
-import search from './plugins/search';
-import awsProton from './plugins/awsProton';
-import awsCodeSuite from './plugins/awsCodeSuite';
-import cookieParser from 'cookie-parser';
-import { PluginEnvironment } from './types';
-import { ServerPermissionClient } from '@backstage/plugin-permission-node';
-import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
-import { createAuthMiddleware } from './alb-auth/middleware';
-import { customErrorHandler } from './middleware/customErrorHandler';
-
-function makeCreateEnv(config: Config) {
- const root = getRootLogger();
- const reader = UrlReaders.default({ logger: root, config });
- const discovery = SingleHostDiscovery.fromConfig(config);
- const cacheManager = CacheManager.fromConfig(config);
- const databaseManager = DatabaseManager.fromConfig(config, { logger: root });
- const tokenManager = ServerTokenManager.fromConfig(config, { logger: root });
- const taskScheduler = TaskScheduler.fromConfig(config);
- const identity = DefaultIdentityClient.create({
- discovery,
- algorithms: ['RS256', 'ES256', 'HS256'],
- });
- const permissions = ServerPermissionClient.fromConfig(config, {
- discovery,
- tokenManager,
- });
-
- root.info(`Created UrlReader ${reader}`);
-
- return (plugin: string): PluginEnvironment => {
- const logger = root.child({ type: 'plugin', plugin });
- const database = databaseManager.forPlugin(plugin);
- const cache = cacheManager.forPlugin(plugin);
- const scheduler = taskScheduler.forPlugin(plugin);
- return {
- logger,
- database,
- cache,
- config,
- reader,
- discovery,
- tokenManager,
- scheduler,
- permissions,
- identity,
- };
- };
-}
-
-async function main() {
- const config = await loadBackendConfig({
- argv: process.argv,
- logger: getRootLogger(),
- });
- const createEnv = makeCreateEnv(config);
-
- const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
- const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
- const authEnv = useHotMemoize(module, () => createEnv('auth'));
- const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
- const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
- const searchEnv = useHotMemoize(module, () => createEnv('search'));
- const appEnv = useHotMemoize(module, () => createEnv('app'));
- const awsProtonEnv = useHotMemoize(module, () =>
- createEnv('aws-proton-backend'),
- );
- const awsCodeSuiteEnv = useHotMemoize(module, () =>
- createEnv('aws-codesuite-backend'),
- );
- const authMiddleware = await createAuthMiddleware(config, appEnv);
-
- const customErrorHandlerMiddleware = customErrorHandler({
- showStackTraces: false,
- });
-
- const apiRouter = Router();
- apiRouter.use(cookieParser());
- apiRouter.use('/catalog', authMiddleware, await catalog(catalogEnv));
- apiRouter.use('/scaffolder', authMiddleware, await scaffolder(scaffolderEnv));
- apiRouter.use('/auth', await auth(authEnv));
- apiRouter.use('/techdocs', authMiddleware, await techdocs(techdocsEnv));
- apiRouter.use('/proxy', authMiddleware, await proxy(proxyEnv));
- apiRouter.use('/search', authMiddleware, await search(searchEnv));
- apiRouter.use('/aws-proton-backend', await awsProton(awsProtonEnv));
- apiRouter.use(
- '/aws-codesuite-backend',
- authMiddleware,
- await awsCodeSuite(awsCodeSuiteEnv),
- );
- apiRouter.use('/cookie', authMiddleware, (_req, res) => {
- res.status(200).send(`Coming right up`);
- });
- apiRouter.use(
- authMiddleware,
- notFoundHandler(),
- );
- // customErrorHandlerMiddleware must be the last middleware to function
- apiRouter.use(customErrorHandlerMiddleware);
-
- const service = createServiceBuilder(module)
- .loadConfig(config)
- .addRouter('/api', apiRouter)
- .addRouter('', await app(appEnv));
-
- await service.start().catch(err => {
- console.log(err);
- process.exit(1);
- });
-}
-
-module.hot?.accept();
-main().catch(error => {
- console.error('Backend failed to start up', error);
- process.exit(1);
-});
diff --git a/source/backstage/packages/backend/src/middleware/customErrorHandler.test.ts b/source/backstage/packages/backend/src/middleware/customErrorHandler.test.ts
deleted file mode 100644
index 6dc74359..00000000
--- a/source/backstage/packages/backend/src/middleware/customErrorHandler.test.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import express from 'express';
-import request from 'supertest';
-import { customErrorHandler } from './customErrorHandler';
-import {
- AuthenticationError,
- ConflictError,
- InputError,
- NotAllowedError,
- NotFoundError,
- NotModifiedError,
-} from '@backstage/errors';
-import createError from 'http-errors';
-
-type ErrorCause = {
- name: string
- message: string
- stack: string
-}
-
-class CustomError extends Error {
- cause: ErrorCause
- constructor(message: string, stack: string) {
- super(message);
- this.name = "CustomError"
- this.cause = {name: "CustomError", message: message, stack: stack};
- this.stack = stack;
- }
-};
-
-describe('customErrorHandler', () => {
- let app: express.Application;
-
- beforeEach(function () {
- app = express();
- });
-
- it('gives default code and message', async () => {
- app.use('/breaks', () => {
- throw new Error('some message');
- });
- app.use(customErrorHandler());
-
- const response = await request(app).get('/breaks');
-
- expect(response.status).toBe(500);
- expect(response.body).toEqual({
- error: expect.objectContaining({
- name: 'Error',
- message: 'some message',
- }),
- request: { method: 'GET', url: '/breaks' },
- response: { statusCode: 500 },
- });
- });
-
- it('does not try to send the response again if it has already been sent', async () => {
- const mockSend = jest.fn();
-
- app.use('/works_with_async_fail', (_, res) => {
- res.status(200).send('hello');
-
- // mutate the response object to test the middleware.
- // it's hard to catch errors inside middleware from the outside.
- res.send = mockSend;
- throw new Error('some message');
- });
-
- app.use(customErrorHandler());
- const response = await request(app).get('/works_with_async_fail');
-
- expect(response.status).toBe(200);
- expect(response.text).toBe('hello');
-
- expect(mockSend).not.toHaveBeenCalled();
- });
-
- it('takes code from http-errors library errors', async () => {
- app.use('/breaks', () => {
- throw createError(432, 'Some Message');
- });
- app.use(customErrorHandler());
-
- const response = await request(app).get('/breaks');
-
- expect(response.status).toBe(432);
- expect(response.body).toEqual({
- error: {
- expose: true,
- name: 'BadRequestError',
- message: 'Some Message',
- status: 432,
- statusCode: 432,
- },
- request: {
- method: 'GET',
- url: '/breaks',
- },
- response: { statusCode: 432 },
- });
- });
-
- it.each([
- ['/NotModifiedError', NotModifiedError, 304],
- ['/InputError', InputError, 400],
- ['/AuthenticationError', AuthenticationError, 401],
- ['/NotAllowedError', NotAllowedError, 403],
- ['/NotFoundError', NotFoundError, 404],
- ['/ConflictError', ConflictError, 409],
- ])('handles well-known error classes', async (path, error, statusCode) => {
- app.use(path, () => {
- throw new error();
- });
- app.use(customErrorHandler());
-
- const r = request(app);
-
- expect((await r.get(path)).status).toBe(statusCode);
- if (statusCode != 304) {
- expect((await r.get(path)).body.error.name).toBe(error.name);
- }
- });
-
- it('logs all 500 errors', async () => {
- const mockLogger = { child: jest.fn(), error: jest.fn() };
- mockLogger.child.mockImplementation(() => mockLogger as any);
-
- const thrownError = new Error('some error');
-
- app.use('/breaks', () => {
- throw thrownError;
- });
- app.use(customErrorHandler({ logger: mockLogger as any }));
-
- await request(app).get('/breaks');
-
- expect(mockLogger.error).toHaveBeenCalledWith(
- 'Request failed with status 500',
- thrownError,
- );
- });
-
- it('does not log 400 errors', async () => {
- const mockLogger = { child: jest.fn(), error: jest.fn() };
- mockLogger.child.mockImplementation(() => mockLogger as any);
-
- app.use('/NotFound', () => {
- throw new NotFoundError();
- });
- app.use(customErrorHandler({ logger: mockLogger as any }));
-
- await request(app).get('/NotFound');
-
- expect(mockLogger.error).not.toHaveBeenCalled();
- });
-
- it('log 400 errors when logClientErrors is true', async () => {
- const mockLogger = { child: jest.fn(), error: jest.fn() };
- mockLogger.child.mockImplementation(() => mockLogger as any);
-
- app.use('/NotFound', () => {
- throw new NotFoundError();
- });
- app.use(customErrorHandler({ logger: mockLogger as any, logClientErrors: true }));
-
- await request(app).get('/NotFound');
-
- expect(mockLogger.error).toHaveBeenCalled();
- });
-
- it('dont show stack trace from error', async () => {
- app.use('/breaks', () => {
- throw new CustomError('some message', 'DANGEROUS STACK TRACE');
- });
- app.use(customErrorHandler({showStackTraces: false}));
-
- const response = await request(app).get('/breaks');
-
- expect(response.status).toBe(500);
- expect(response.body).toEqual({
- error: {
- name: 'CustomError',
- message: 'some message',
- },
- request: { method: 'GET', url: '/breaks' },
- response: { statusCode: 500 },
- });
- });
-
- it('shows stack trace from error', async () => {
- app.use('/breaks', () => {
- throw new CustomError('some message', 'DANGEROUS STACK TRACE');
- });
- app.use(customErrorHandler({showStackTraces: true}));
-
- const response = await request(app).get('/breaks');
-
- expect(response.status).toBe(500);
- expect(response.body).toEqual({
- error: {
- name: 'CustomError',
- message: 'some message',
- stack: 'DANGEROUS STACK TRACE',
- cause: {
- name: 'CustomError',
- message: 'some message',
- stack: 'DANGEROUS STACK TRACE',
- }
- },
- request: { method: 'GET', url: '/breaks' },
- response: { statusCode: 500 },
- });
- });
-});
diff --git a/source/backstage/packages/backend/src/plugins/app.ts b/source/backstage/packages/backend/src/plugins/app.ts
deleted file mode 100644
index 4539810b..00000000
--- a/source/backstage/packages/backend/src/plugins/app.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { createRouter } from '@backstage/plugin-app-backend';
-import { Router } from 'express';
-import { PluginEnvironment } from '../types';
-
-export default async function createPlugin(
- env: PluginEnvironment,
-): Promise {
- return await createRouter({
- logger: env.logger,
- config: env.config,
- database: env.database,
- appPackageName: 'app',
- });
-}
diff --git a/source/backstage/packages/backend/src/plugins/awsCodeSuite.ts b/source/backstage/packages/backend/src/plugins/awsCodeSuite.ts
deleted file mode 100644
index b5d2dfb4..00000000
--- a/source/backstage/packages/backend/src/plugins/awsCodeSuite.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { createRouter } from '@aws/aws-codeservices-backend-plugin-for-backstage';
-import { PluginEnvironment } from '../types';
-
-export default async function createPlugin(env: PluginEnvironment) {
- return await createRouter({
- logger: env.logger,
- config: env.config,
- });
-}
diff --git a/source/backstage/packages/backend/src/plugins/awsProton.ts b/source/backstage/packages/backend/src/plugins/awsProton.ts
deleted file mode 100644
index 1d719458..00000000
--- a/source/backstage/packages/backend/src/plugins/awsProton.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { createRouter } from '@aws/aws-proton-backend-plugin-for-backstage';
-import { PluginEnvironment } from '../types';
-
-export default async function createPlugin(env: PluginEnvironment) {
- return await createRouter({
- logger: env.logger,
- config: env.config,
- });
-}
diff --git a/source/backstage/packages/backend/src/plugins/catalog.ts b/source/backstage/packages/backend/src/plugins/catalog.ts
deleted file mode 100644
index 75320d1a..00000000
--- a/source/backstage/packages/backend/src/plugins/catalog.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
-import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
-import { AwsS3EntityProvider } from '@backstage/plugin-catalog-backend-module-aws';
-import { Router } from 'express';
-import { PluginEnvironment } from '../types';
-
-export default async function createPlugin(
- env: PluginEnvironment,
-): Promise {
- const builder = await CatalogBuilder.create(env);
- builder.addEntityProvider(
- AwsS3EntityProvider.fromConfig(env.config, {
- logger: env.logger,
- scheduler: env.scheduler,
- }),
- );
- builder.addProcessor(new ScaffolderEntitiesProcessor());
- const { processingEngine, router } = await builder.build();
- await processingEngine.start();
- return router;
-}
diff --git a/source/backstage/packages/backend/src/plugins/proxy.ts b/source/backstage/packages/backend/src/plugins/proxy.ts
deleted file mode 100644
index eaaf94a6..00000000
--- a/source/backstage/packages/backend/src/plugins/proxy.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { createRouter } from '@backstage/plugin-proxy-backend';
-import { Router } from 'express';
-import { PluginEnvironment } from '../types';
-
-export default async function createPlugin(
- env: PluginEnvironment,
-): Promise {
- return await createRouter({
- logger: env.logger,
- config: env.config,
- discovery: env.discovery,
- });
-}
diff --git a/source/backstage/packages/backend/src/plugins/s3-catalog-action.ts b/source/backstage/packages/backend/src/plugins/s3-catalog-action.ts
deleted file mode 100644
index 85980e39..00000000
--- a/source/backstage/packages/backend/src/plugins/s3-catalog-action.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { Config } from '@backstage/config';
-import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
-import { v4 as uuidv4 } from 'uuid';
-import * as yaml from 'yaml';
-import { z } from 'zod';
-
-import { DefaultAwsCredentialsManager } from '@backstage/integration-aws-node';
-
-import {
- PutObjectCommand,
- PutObjectCommandInput,
- S3Client,
-} from '@aws-sdk/client-s3';
-
-export const createNewCatalogInfoAction = (options: { config: Config }) => {
- const { config } = options;
- const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config);
-
- const bucketName = config.getString('s3-catalog.bucketName');
- const region = config.getString('s3-catalog.region');
- const catalogPrefix = config.getString('s3-catalog.prefix');
-
- return createTemplateAction({
- id: 'aws:s3:catalog:write',
- description:
- 'Writes the catalog-info.yaml for your template to the backend s3 bucket',
- schema: {
- input: z.object({
- componentId: z
- .string()
- .describe(
- 'The unique component id which is used for the catalog-info name',
- ),
- entity: z
- .record(z.any())
- .describe('YAML body for the catalog-info.yaml content'),
- }),
- output: {
- type: 'object',
- properties: {
- s3Url: {
- title: 'S3 URL Path file was upload to',
- type: 'string',
- },
- s3Uri: {
- title: 'S3 URI Path file was upload to',
- type: 'string',
- },
- },
- },
- },
-
- async handler(ctx) {
- const creds = await awsCredentialsManager.getCredentialProvider();
-
- const client = new S3Client({
- region: region,
- customUserAgent: 'aws-s3-upload-backstage',
- credentialDefaultProvider: () => creds.sdkCredentialProvider,
- });
-
- const catalogUUID = uuidv4();
-
- const keyPath = `${catalogPrefix}/catalog-info-${ctx.input.componentId}-${catalogUUID}.yaml`;
- const input: PutObjectCommandInput = {
- Body: yaml.stringify(ctx.input.entity),
- Bucket: bucketName,
- Key: keyPath,
- };
-
- const resp = await client.send(new PutObjectCommand(input));
-
- const s3Endpoint = `s3.${region}.amazonaws.com`;
-
- if (resp.ETag !== undefined) {
- ctx.logger.info(
- `Successfully created s3 object s3://${input.Bucket}/${input.Key}`,
- );
- ctx.output('s3Url', `https://${bucketName}.${s3Endpoint}/${keyPath}`);
- ctx.output('s3Uri', `s3://${bucketName}/${keyPath}`);
- }
- },
- });
-};
diff --git a/source/backstage/packages/backend/src/plugins/scaffolder.ts b/source/backstage/packages/backend/src/plugins/scaffolder.ts
deleted file mode 100644
index 352c5310..00000000
--- a/source/backstage/packages/backend/src/plugins/scaffolder.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { CatalogClient } from '@backstage/catalog-client';
-import { Router } from 'express';
-import type { PluginEnvironment } from '../types';
-import { ScmIntegrations } from '@backstage/integration';
-import {
- createBuiltinActions,
- createRouter,
-} from '@backstage/plugin-scaffolder-backend';
-import { createAwsProtonServiceAction } from '@aws/aws-proton-backend-plugin-for-backstage';
-import { createNewCatalogInfoAction } from './s3-catalog-action';
-import { createNewYamlFileAction } from './yaml-fs-writer';
-
-export default async function createPlugin(
- env: PluginEnvironment,
-): Promise {
- const catalogClient = new CatalogClient({
- discoveryApi: env.discovery,
- });
-
- const integrations = ScmIntegrations.fromConfig(env.config);
-
- const builtInActions = createBuiltinActions({
- integrations,
- catalogClient,
- reader: env.reader,
- config: env.config,
- });
-
- const actions = [
- ...builtInActions,
- createAwsProtonServiceAction({ config: env.config }),
- createNewCatalogInfoAction({ config: env.config }),
- createNewYamlFileAction()
- ];
-
- return await createRouter({
- logger: env.logger,
- config: env.config,
- database: env.database,
- reader: env.reader,
- catalogClient,
- actions,
- identity: env.identity,
- permissions: env.permissions,
- });
-}
diff --git a/source/backstage/packages/backend/src/plugins/techdocs.ts b/source/backstage/packages/backend/src/plugins/techdocs.ts
deleted file mode 100644
index a69f5ba3..00000000
--- a/source/backstage/packages/backend/src/plugins/techdocs.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { DockerContainerRunner } from '@backstage/backend-common';
-import {
- createRouter,
- Generators,
- Preparers,
- Publisher,
-} from '@backstage/plugin-techdocs-backend';
-import Docker from 'dockerode';
-import { Router } from 'express';
-import { PluginEnvironment } from '../types';
-
-export default async function createPlugin(
- env: PluginEnvironment,
-): Promise {
- // Preparers are responsible for fetching source files for documentation.
- const preparers = await Preparers.fromConfig(env.config, {
- logger: env.logger,
- reader: env.reader,
- });
-
- // Docker client (conditionally) used by the generators, based on techdocs.generators config.
- const dockerClient = new Docker();
- const containerRunner = new DockerContainerRunner({ dockerClient });
-
- // Generators are used for generating documentation sites.
- const generators = await Generators.fromConfig(env.config, {
- logger: env.logger,
- containerRunner,
- });
-
- // Publisher is used for
- // 1. Publishing generated files to storage
- // 2. Fetching files from storage and passing them to TechDocs frontend.
- const publisher = await Publisher.fromConfig(env.config, {
- logger: env.logger,
- discovery: env.discovery,
- });
-
- // checks if the publisher is working and logs the result
- await publisher.getReadiness();
-
- return await createRouter({
- preparers,
- generators,
- publisher,
- logger: env.logger,
- config: env.config,
- discovery: env.discovery,
- cache: env.cache,
- });
-}
diff --git a/source/backstage/packages/backend/src/plugins/yaml-fs-writer.ts b/source/backstage/packages/backend/src/plugins/yaml-fs-writer.ts
deleted file mode 100644
index 04d9c42f..00000000
--- a/source/backstage/packages/backend/src/plugins/yaml-fs-writer.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
-import * as yaml from 'yaml';
-import { z } from 'zod';
-import * as fs from 'fs'
-
-export const createNewYamlFileAction = () => {
-
- return createTemplateAction({
- id: 'aws:fs:write-yaml',
- description:
- 'Writes the input as a workspace file',
- schema: {
- input: z.object({
- filename: z
- .string()
- .describe(
- 'The filename to write',
- ),
- entity: z
- .record(z.any())
- .describe('YAML body for the file content'),
- }),
- output: {
- type: 'object',
- properties: {
- filePath: {
- title: 'Workspace path file was written to',
- type: 'string',
- },
- },
- },
- },
-
- async handler(ctx) {
-
- const filepath = `${ctx.workspacePath}/${ctx.input.filename}`
-
- fs.writeFileSync(filepath, yaml.stringify(ctx.input.entity))
-
- ctx.logger.info(
- `Successfully created file: ${ctx.input.filename}`,
- );
-
- ctx.output('filename', ctx.input.filename);
- },
- });
-};
diff --git a/source/backstage/packages/backend/src/types.ts b/source/backstage/packages/backend/src/types.ts
deleted file mode 100644
index 72804097..00000000
--- a/source/backstage/packages/backend/src/types.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { Logger } from 'winston';
-import { Config } from '@backstage/config';
-import {
- PluginCacheManager,
- PluginDatabaseManager,
- PluginEndpointDiscovery,
- TokenManager,
- UrlReader,
-} from '@backstage/backend-common';
-import { PluginTaskScheduler } from '@backstage/backend-tasks';
-import { PermissionEvaluator } from '@backstage/plugin-permission-common';
-import { IdentityApi } from '@backstage/plugin-auth-node';
-
-export type PluginEnvironment = {
- logger: Logger;
- database: PluginDatabaseManager;
- cache: PluginCacheManager;
- config: Config;
- reader: UrlReader;
- discovery: PluginEndpointDiscovery;
- tokenManager: TokenManager;
- scheduler: PluginTaskScheduler;
- permissions: PermissionEvaluator;
- identity: IdentityApi;
-};
diff --git a/source/infrastructure/.cdk-nag-suppression-list.json b/source/infrastructure/.cdk-nag-suppression-list.json
deleted file mode 100644
index e7bf30ae..00000000
--- a/source/infrastructure/.cdk-nag-suppression-list.json
+++ /dev/null
@@ -1,351 +0,0 @@
-{
- "/cms-dev/cms-pipelines/backend-secret/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-SMG4",
- "reason": "Rotating this type of secret is currently not supported; it will require a simple rotation lambda."
- }
- ]
- },
- "/cms-dev/cms-custom-resource/lambda-function/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-L1",
- "reason": "Some libraries used throughout the solution are not yet supported in Python 3.11. For consistency, all lambdas are currently kept at Python 3.10. Future refactoring of unsupported libraries will enable the use of 3.11 throughout the solution."
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-code-pipeline/ArtifactsBucket/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-S1",
- "reason": "An artifact bucket does not need S3 bucket for access logs"
- }
- ]
- },
- "/cms-dev/cms-pipelines/cms-vpc-cloudwatch-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource:::log-stream:*"
- ],
- "reason": "Log stream has to be a wildcard"
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-deploy-role/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Action::kms:GenerateDataKey*",
- "Action::kms:ReEncrypt*",
- "Resource::/*",
- "Action::s3:Abort*",
- "Action::s3:DeleteObject*",
- "Action::s3:List*",
- "Action::s3:GetBucket*",
- "Action::s3:GetObject*"
- ],
- "reason": "Pipelines default role policy is least privilege."
- },
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::arn::logs:::log-group:/aws/codebuild/:*",
- "Resource::arn::codebuild:::report-group/-*",
- "Resource::arn::logs:::log-group:/aws/codebuild/:*",
- "Resource::arn::codebuild:::report-group/-*"
- ],
- "reason": "Pipelines default role policy is least privilege."
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-build-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Action::secretsmanager:*",
- "Action::ssm:*",
- "Resource::arn::ssm:::*",
- "Resource::arn::ssm:::parameter/dev/*",
- "Resource::arn::ssm:::parameter:/dev/cms/*",
- "Resource::arn::secretsmanager:::secret:/dev/cms-backstage/*",
- "Resource::arn::secretsmanager:::secret:cms/*"
- ],
- "reason": "Pipeline creates and reads multiple secrets and SSM parameters."
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-deploy-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::arn::iam:::role/cdk-*"
- ],
- "reason": "CDK role id is not known"
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-build-role/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Action::kms:GenerateDataKey*",
- "Action::kms:ReEncrypt*",
- "Resource::arn::codebuild:::report-group/-*",
- "Resource::arn::logs:::log-group:/aws/codebuild/:*",
- "Resource::/*",
- "Action::s3:Abort*",
- "Action::s3:DeleteObject*",
- "Action::s3:List*",
- "Action::s3:GetBucket*",
- "Action::s3:GetObject*",
- "Action::ecr:*",
- "Resource::*"
- ],
- "reason": "Pipelines default role policy is least privilege."
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-pipeline-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Action::secretsmanager:*",
- "Resource::arn::secretsmanager:::secret:/dev/cms-backstage/*",
- "Resource::arn::secretsmanager:::secret:cms/*",
- "Resource::arn::ssm:::parameter:/dev/cms/*"
- ],
- "reason": "Pipeline creates and reads multiple secrets and SSM parameters."
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-code-pipeline/Source-Stage-Backstage/S3-Source-Backstage-Asset/CodePipelineActionRole/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Action::kms:GenerateDataKey*",
- "Action::kms:ReEncrypt*",
- "Resource::/*",
- "Action::s3:Abort*",
- "Action::s3:DeleteObject*",
- "Action::s3:List*",
- "Action::s3:GetBucket*",
- "Action::s3:GetObject*"
- ],
- "reason": "Pipelines default role policy is least privilege."
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-pipeline-role/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Action::kms:GenerateDataKey*",
- "Action::kms:ReEncrypt*",
- "Resource::/*",
- "Action::s3:Abort*",
- "Action::s3:DeleteObject*",
- "Action::s3:List*",
- "Action::s3:GetBucket*",
- "Action::s3:GetObject*"
- ],
- "reason": "Pipelines default role policy is least privilege."
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-code-pipeline/Source-Stage-Backstage/Source/CodePipelineActionRole/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Action::kms:GenerateDataKey*",
- "Action::kms:ReEncrypt*",
- "Resource::/*",
- "Action::s3:Abort*",
- "Action::s3:DeleteObject*",
- "Action::s3:List*",
- "Action::s3:GetBucket*",
- "Action::s3:GetObject*"
- ],
- "reason": "Pipelines default role policy is least privilege."
- }
- ]
- },
- "/cms-dev/cms-proton-environment/proton-code-build-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::arn:aws:s3:::cdk-*-assets--",
- "Resource::arn:aws:iam:::role/cdk-*-cfn-exec-role--",
- "Resource::arn:aws:iam:::role/cdk-*-file-publishing-role--",
- "Resource::arn::ssm:::parameter/cdk-bootstrap/*/*"
- ],
- "reason": "The * here is the cloudformation buckets generated value, we do not have control over that, hence it has to be wildcard to allow proper functioning here. The last resource here is a parameter one of the wildcard is again the same cloudformation generated id, and other is version"
- },
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::arn::logs:::log-group:/aws/codebuild/AWSProton-*:log-stream:*",
- "Resource::arn::logs:::log-group:/aws/codebuild/AWSProton-*"
- ],
- "reason": "No way to create a log group for codebuild in advance hence this is the least possible privilege"
- },
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::arn::cloudformation:::stack/cms-environment/*",
- "Resource::arn::cloudformation:::stack/CDKToolkit/*"
- ],
- "reason": "We cannot establish stack id in advance hence that has to be a wildcard"
- },
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::arn::proton:::service/*"
- ],
- "reason": "We cannot establish services in advance hence that has to be a wildcard"
- }
- ]
- },
- "/cms-dev/cms-proton-environment/proton-code-build-role/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::arn:aws:iam:::role/cdk-*-file-publishing-role--",
- "Resource::arn:aws:iam:::role/cdk-*-file-publishing-role--",
- "Resource::arn:aws:iam:::role/cdk-*-deploy-role--"
- ],
- "reason": "These are least possible privileges"
- }
- ]
- },
- "/cms-dev/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::/*",
- "Action::s3:Abort*",
- "Action::s3:DeleteObject*",
- "Resource::arn::s3:::cdk-hnb659fds-assets--/*",
- "Action::s3",
- "Action::s3:GetBucket*",
- "Action::s3:GetObject*",
- "Action::s3:List*",
- "Action::kms:ReEncrypt*",
- "Action::kms:GenerateDataKey*"
- ],
- "reason": "Custom bucket deployment manages its own policy and needs these permission for creation of resource"
- },
- {
- "id": "AwsSolutions-IAM4",
- "appliesTo": [
- "Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
- ],
- "reason": "Custom bucket deployment can have a default role"
- }
- ]
- },
- "/cms-dev/cms-custom-resource/lambda-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::arn::logs:::log-group:/aws/lambda/cms-dev-custom-resource:log-stream:*"
- ],
- "reason": "These are least possible privileges"
- }
- ]
- },
- "/cms-dev/cms-proton-environment/custom-resource-policy/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::/*",
- "Resource::/cms_environment_templates/*"
- ],
- "reason": "These are least possible privileges"
- }
- ]
- },
- "/cms-dev/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM4",
- "appliesTo": [
- "Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
- ],
- "reason": "Custom bucket deployment can have a default role"
- }
- ]
- },
- "/cms-dev/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-L1",
- "reason": "Cannot update runtime of the lambda function because it belongs to an AWS managed construct."
- }
- ]
- },
- "/cms-dev/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM4",
- "appliesTo": [
- "Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
- ],
- "reason": "Log retention lambda uses managed policies."
- }
- ]
- },
- "/cms-dev/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::*"
- ],
- "reason": "Log retention lambda uses managed policies which have wildcard permissions."
- }
- ]
- },
- "/cms-dev/cms-metrics/metrics-reporting-lambda-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::arn::logs:::log-group:/aws/lambda/cms-dev-anonymous-metrics-reporting:log-stream:*"
- ],
- "reason": "Log retention lambda uses managed policies which have wildcard permissions."
- },
- {
- "id": "AwsSolutions-IAM5",
- "appliesTo": [
- "Resource::*"
- ],
- "reason": "CloudWatch Metrics do not support any kind of policy limitation via resource id or condition"
- }
- ]
- },
- "/cms-dev/cms-metrics/cmdp-metrics-lambda/Resource": {
- "rules_to_suppress": [
- {
- "id": "AwsSolutions-L1",
- "reason": "Some libraries used throughout the solution are not yet supported in Python 3.11. For consistency, all lambdas are currently kept at Python 3.10. Future refactoring of unsupported libraries will enable the use of 3.11 throughout the solution."
- }
- ]
- }
-
-}
diff --git a/source/infrastructure/.cfn-nag-suppression-list.json b/source/infrastructure/.cfn-nag-suppression-list.json
deleted file mode 100644
index 205366f7..00000000
--- a/source/infrastructure/.cfn-nag-suppression-list.json
+++ /dev/null
@@ -1,231 +0,0 @@
-{
- "/cms-dev/cms-pipelines/backend-secret/Resource": {
- "rules_to_suppress": [
- {
- "id": "W77",
- "reason": "Rotating this type of secret is currently not supported; it will require a simple rotation lambda."
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-code-pipeline/ArtifactsBucket/Resource": {
- "rules_to_suppress": [
- {
- "id": "W35",
- "reason": "An artifact bucket does not need S3 bucket for access logs"
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-deploy-role/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "W12",
- "reason": "Pipelines default role policy is least privilege."
- },
- {
- "id": "W76",
- "reason": "It is a large default policy"
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-build-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "F3",
- "reason": "Pipeline creates and reads multiple secrets."
- },
- {
- "id": "W28",
- "reason": "CDK role id is not known"
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-deploy-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "W28",
- "reason": "CDK role id is not known"
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-build-role/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "F4",
- "reason": "Pipelines default role policy is least privilege."
- },
- {
- "id": "W12",
- "reason": "Pipelines default role policy is least privilege."
- },
- {
- "id": "W76",
- "reason": "It is a large default policy"
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-pipeline-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "F3",
- "reason": "Pipeline creates and reads multiple secrets."
- },
- {
- "id": "W28",
- "reason": "CDK role id is not known"
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-pipeline-role/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "W12",
- "reason": "Pipelines default role policy is least privilege."
- },
- {
- "id": "W76",
- "reason": "It is a large default policy"
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-code-pipeline/Source-Stage-Backstage/Source/CodePipelineActionRole/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "W12",
- "reason": "Pipelines default role policy is least privilege."
- },
- {
- "id": "W76",
- "reason": "It is a large default policy"
- }
- ]
- },
- "/cms-dev/cms-pipelines/cms-vpc/publicSubnet1/Subnet": {
- "rules_to_suppress": [
- {
- "id": "W33",
- "reason": "EC2 Subnet should not have MapPublicIpOnLaunch set to true"
- }
- ]
- },
- "/cms-dev/cms-pipelines/cms-vpc/publicSubnet2/Subnet": {
- "rules_to_suppress": [
- {
- "id": "W33",
- "reason": "EC2 Subnet should not have MapPublicIpOnLaunch set to true"
- }
- ]
- },
- "/cms-dev/cms-pipelines/cms-vpc-log-group/Resource": {
- "rules_to_suppress": [
- {
- "id": "W84",
- "reason": "CloudWatchLogs LogGroup should specify a KMS Key Id to encrypt the log data"
- },
- {
- "id": "W86",
- "reason": "Its important that customer can retain logs as long as they want, they can change the retention period if they want"
- }
- ]
- },
- "/cms-dev/cms-pipelines/backstage-ecr/Resource": {
- "rules_to_suppress": [
- {
- "id": "W28",
- "reason": "Resource found with an explicit name, this disallows updates that require replacement of this resource"
- }
- ]
- },
- "/cms-dev/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource": {
- "rules_to_suppress": [
- {
- "id": "W58",
- "reason": "Automatically created lambda by CustomDeployment, does not need log permissions"
- },
- {
- "id": "W89",
- "reason": "Custom resource lambda only use during stack creation process, can be outside vpc for now"
- },
- {
- "id": "W92",
- "reason": "No need to define ReservedConcurrentExecutions for custom deployment lambda"
- }
- ]
- },
- "/cms-dev/cms-custom-resource/lambda-function/Resource": {
- "rules_to_suppress": [
- {
- "id": "W89",
- "reason": "Custom resource lambda only use during stack creation process, can be outside vpc for now"
- },
- {
- "id": "W92",
- "reason": "No need to define ReservedConcurrentExecutions for custom resource lambda"
- }
- ]
- },
- "/cms-dev/cms-proton-environment/custom-resource-policy/Resource": {
- "rules_to_suppress": [
- {
- "id": "W76",
- "reason": "IAM Policy is least privileged"
- }
- ]
- },
- "/cms-dev/cms-proton-environment/proton-log-bucket/Resource": {
- "rules_to_suppress": [
- {
- "id": "W35",
- "reason": "log bucket does not require access logging"
- },
- {
- "id": "W41",
- "reason": "log bucket does not allow customer managed encryption"
- }
- ]
- },
- "/cms-dev/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/DefaultPolicy/Resource": {
- "rules_to_suppress": [
- {
- "id": "W12",
- "reason": "Log retention lambda uses managed policies that use wildcard permissions."
- }
- ]
- },
- "/cms-dev/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Resource": {
- "rules_to_suppress": [
- {
- "id": "W58",
- "reason": "Automatically created lambda by Lambda Function construct, does not need log permissions"
- },
- {
- "id": "W89",
- "reason": "Log retention lambda can be outside vpc for now"
- },
- {
- "id": "W92",
- "reason": "No need to define ReservedConcurrentExecutions for log retention lambda"
- }
- ]
- },
- "/cms-dev/cms-metrics/cmdp-metrics-lambda/Resource": {
- "rules_to_suppress": [
- {
- "id": "W89",
- "reason": "Custom resource lambda only use during stack creation process, can be outside vpc for now"
- },
- {
- "id": "W92",
- "reason": "No need to define ReservedConcurrentExecutions for custom deployment lambda"
- }
- ]
- },
- "/cms-dev/cms-metrics/metrics-reporting-lambda-role/Resource": {
- "rules_to_suppress": [
- {
- "id": "W11",
- "reason": "Wildcard permission is necessary to gather cloudwatch metrics"
- }
- ]
-
- }
-}
diff --git a/source/infrastructure/__init__.py b/source/infrastructure/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/infrastructure/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/infrastructure/app.py b/source/infrastructure/app.py
deleted file mode 100644
index c458566d..00000000
--- a/source/infrastructure/app.py
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from os.path import dirname, realpath
-
-# Third Party Libraries
-import aws_cdk
-from cdk_nag import AwsSolutionsChecks
-
-# Connected Mobility Solution on AWS
-from .aspects.nag_suppression import NagSuppression, NagType
-from .stacks import CmsConstants
-from .stacks.cms_stack import CmsStack
-
-app = aws_cdk.App()
-stack = CmsStack(
- app,
- CmsConstants.STACK_NAME,
- description=(
- f"({CmsConstants.SOLUTION_ID}) "
- f"{CmsConstants.SOLUTION_NAME}. "
- f"Version {CmsConstants.SOLUTION_VERSION}"
- ),
-)
-
-
-aws_cdk.Tags.of(app).add("Solutions:ModuleName", CmsConstants.MODULE_NAME)
-aws_cdk.Tags.of(app).add("Solutions:SolutionName", CmsConstants.SOLUTION_NAME)
-aws_cdk.Tags.of(app).add("Solutions:SolutionID", CmsConstants.SOLUTION_ID)
-aws_cdk.Tags.of(app).add("Solutions:SolutionVersion", CmsConstants.SOLUTION_VERSION)
-aws_cdk.Tags.of(app).add("Solutions:ApplicationType", CmsConstants.APPLICATION_TYPE)
-
-# CDK and CFN nags
-aws_cdk.Aspects.of(app).add(
- NagSuppression(
- f"{dirname(realpath(__file__))}/.cdk-nag-suppression-list.json", NagType.CDK_NAG
- )
-)
-aws_cdk.Aspects.of(app).add(
- NagSuppression(
- f"{dirname(realpath(__file__))}/.cfn-nag-suppression-list.json", NagType.CFN_NAG
- )
-)
-if app.node.try_get_context("nag-enforce"):
- aws_cdk.Aspects.of(app).add(AwsSolutionsChecks())
-
-app.synth()
diff --git a/source/infrastructure/aspects/__init__.py b/source/infrastructure/aspects/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/infrastructure/aspects/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/infrastructure/aspects/nag_suppression.py b/source/infrastructure/aspects/nag_suppression.py
deleted file mode 100644
index 7c3c7cfd..00000000
--- a/source/infrastructure/aspects/nag_suppression.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-import json
-from enum import Enum
-
-# Third Party Libraries
-import jsii
-from aws_cdk import CfnResource, IAspect
-from constructs import IConstruct
-
-
-class NagType(Enum):
- CDK_NAG = "cdk_nag"
- CFN_NAG = "cfn_nag"
-
-
-@jsii.implements(IAspect)
-class NagSuppression:
- def __init__(self, suppression_file_path: str, nag_type: NagType) -> None:
- with open(suppression_file_path, encoding="UTF-8") as suppression_file:
- self.suppressions = dict(json.loads(suppression_file.read()))
- self.nag_type = nag_type
-
- # Visits every resource defined in cfn template and applies suppression metadata by resource path from the suppresions file provided
- # Resource paths in our suppression lists must be to L1 constructs. When visiting an L2 construct, the path will not match
- # and the resource will be skipped, however, the supporting L1 construct which eventually be visited, and the suppression will be added then
- def visit(self, node: IConstruct) -> None:
- node_path = f"/{node.node.path}"
- suppression_metadata = self.suppressions.get(node_path)
-
- if suppression_metadata:
- CfnResource.add_metadata(
- node, key=self.nag_type.value, value=suppression_metadata # type: ignore
- )
diff --git a/source/infrastructure/constructs/__init__.py b/source/infrastructure/constructs/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/infrastructure/constructs/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/infrastructure/constructs/app_registry.py b/source/infrastructure/constructs/app_registry.py
deleted file mode 100644
index 24ad71af..00000000
--- a/source/infrastructure/constructs/app_registry.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Third Party Libraries
-from aws_cdk import Stack, aws_servicecatalogappregistry
-from constructs import Construct
-
-
-class AppRegistryConstruct(Construct):
- def __init__(
- self,
- scope: Construct,
- construct_id: str,
- application_name: str,
- application_type: str,
- solution_id: str,
- solution_name: str,
- solution_version: str,
- ) -> None:
- super().__init__(scope, construct_id)
-
- region = Stack.of(self).region
- account = Stack.of(self).account
-
- cfn_application = aws_servicecatalogappregistry.CfnApplication(
- self,
- "app-registry-application",
- name=f"{application_name}-{region}-{account}",
- )
-
- attribute_group = aws_servicecatalogappregistry.CfnAttributeGroup(
- self,
- "default-application-attributes",
- name=f"{application_name}-{region}-{account}",
- description="Attribute group for solution information",
- attributes={
- "ApplicationType": application_type,
- "Version": solution_version,
- "SolutionID": solution_id,
- "SolutionName": solution_name,
- },
- )
-
- # Associate attribute group with registry
- aws_servicecatalogappregistry.CfnAttributeGroupAssociation(
- self,
- "app-registry-application-attribute-association",
- application=cfn_application.attr_id,
- attribute_group=attribute_group.attr_id,
- )
-
- # Associate stacks with application registry, including this stack.
- for child in Stack.of(self).node.find_all():
- if Stack.is_stack(child):
- stack = Stack.of(child)
- aws_servicecatalogappregistry.CfnResourceAssociation(
- stack,
- "app-registry-application-stack-association",
- application=cfn_application.attr_id,
- resource=stack.stack_id,
- resource_type="CFN_STACK",
- )
diff --git a/source/infrastructure/constructs/custom_resource_lambda.py b/source/infrastructure/constructs/custom_resource_lambda.py
deleted file mode 100644
index 138c235b..00000000
--- a/source/infrastructure/constructs/custom_resource_lambda.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Third Party Libraries
-from aws_cdk import Duration, aws_iam, aws_lambda, aws_logs
-from constructs import Construct
-
-# Connected Mobility Solution on AWS
-from ..stacks import CmsConstants, generate_lambda_cloudwatch_logs_policy_document
-
-
-class CustomResourceLambdaConstruct(Construct):
- def __init__(
- self,
- scope: Construct,
- construct_id: str,
- dependency_layer: aws_lambda.LayerVersion,
- ) -> None:
- super().__init__(scope, construct_id)
- custom_resource_lambda_name = f"{CmsConstants.STACK_NAME}-custom-resource"
-
- self.custom_resource_lambda_role = aws_iam.Role(
- self,
- "lambda-role",
- assumed_by=aws_iam.ServicePrincipal("lambda.amazonaws.com"),
- inline_policies={
- "lambda-logs-policy": generate_lambda_cloudwatch_logs_policy_document(
- self, custom_resource_lambda_name
- ),
- },
- )
-
- self.custom_resource_lambda = aws_lambda.Function(
- self,
- "lambda-function",
- code=aws_lambda.Code.from_asset(
- "source/infrastructure/handlers/custom_resource"
- ),
- handler="custom_resource.handler",
- function_name=custom_resource_lambda_name,
- role=self.custom_resource_lambda_role,
- runtime=aws_lambda.Runtime.PYTHON_3_10,
- timeout=Duration.minutes(5),
- layers=[dependency_layer],
- environment={"USER_AGENT_STRING": CmsConstants.USER_AGENT_STRING},
- log_retention=aws_logs.RetentionDays.THREE_MONTHS,
- )
-
- def add_policy_to_custom_resource_lambda(self, policy: aws_iam.Policy) -> None:
- self.custom_resource_lambda_role.attach_inline_policy(policy)
diff --git a/source/infrastructure/constructs/lambda_dependencies.py b/source/infrastructure/constructs/lambda_dependencies.py
deleted file mode 100644
index fdb31078..00000000
--- a/source/infrastructure/constructs/lambda_dependencies.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-import os
-import pathlib
-from io import TextIOWrapper
-from os.path import abspath, dirname
-from typing import Any
-
-# Third Party Libraries
-import toml
-from aws_cdk import aws_lambda
-from constructs import Construct
-
-
-class LambdaDependenciesConstruct(Construct):
- def __init__(
- self,
- scope: Construct,
- construct_id: str,
- dependency_layer_dir_name: str,
- **kwargs: Any,
- ) -> None:
- super().__init__(scope, construct_id, **kwargs)
-
- dir_path = f"{os.getcwd()}/source/infrastructure/{dependency_layer_dir_name}"
- project_dir = f"{dirname(dirname(dirname(dirname(abspath(__file__)))))}"
- source_pipfile = f"{project_dir}/Pipfile"
- pip_path = f"{dir_path}/python"
-
- # Create the folders out to the build directory
- pathlib.Path(pip_path).mkdir(parents=True, exist_ok=True)
- requirements = f"{dir_path}/requirements.txt"
-
- # Copy Pipfile to build directory as requirements.txt format and excluding the large packages
- with open(source_pipfile, "r", encoding="utf-8") as pipfile:
- new_pipfile = toml.load(pipfile)
- with open(requirements, "w", encoding="utf-8") as requirements_file:
-
- for package, constraint in new_pipfile["packages"].items():
- if package not in ["boto3", "aws-cdk-lib"]:
- self.req_formatter(
- package=package,
- constraint=constraint,
- requirements_file=requirements_file,
- )
-
- # Install the requirements in the build directory (CDK will use this whole folder to build the zip)
- os.system( # nosec
- f"/bin/bash -c 'python -m pip install -q --upgrade --target {pip_path} --requirement {requirements}'"
- # f" && find {dir_path} -name \\*.so -exec strip \\{{\\}} \\;'"
- )
-
- self.dependency_layer = aws_lambda.LayerVersion(
- self,
- "lambda-dependency-layer-version",
- code=aws_lambda.Code.from_asset(dir_path),
- compatible_architectures=[
- aws_lambda.Architecture.X86_64,
- aws_lambda.Architecture.ARM_64,
- ],
- compatible_runtimes=[
- aws_lambda.Runtime.PYTHON_3_8,
- aws_lambda.Runtime.PYTHON_3_9,
- aws_lambda.Runtime.PYTHON_3_10,
- ],
- )
-
- def req_formatter(
- self, package: str, constraint: Any, requirements_file: TextIOWrapper
- ) -> None:
- if constraint == "*":
- requirements_file.write(package + "\n")
- else:
- try:
- extras = (
- str(constraint.get("extras", "all"))
- .replace("'", "")
- .replace('"', "")
- )
-
- # Requirements.txt wildcards are done by not specifying a version, replace with empty string instead
- version = constraint["version"] if constraint["version"] != "*" else ""
-
- requirements_file.write(f"{package}{extras} {version}\n")
- except (TypeError, KeyError, AttributeError):
- if isinstance(constraint, str):
- requirements_file.write(f"{package} {constraint}\n")
diff --git a/source/infrastructure/constructs/module_integration.py b/source/infrastructure/constructs/module_integration.py
deleted file mode 100644
index 677371eb..00000000
--- a/source/infrastructure/constructs/module_integration.py
+++ /dev/null
@@ -1,92 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import cast
-
-# Third Party Libraries
-from aws_cdk import CfnCondition, CfnResource, aws_ssm
-from constructs import Construct
-
-# Connected Mobility Solution on AWS
-from ..stacks import CmsConstants
-
-
-class ModuleOutputsConstruct(Construct):
- # pylint: disable=too-many-arguments
- def __init__(
- self,
- scope: Construct,
- construct_id: str,
- deployment_uuid: str,
- resource_bucket: str,
- resource_bucket_region: str,
- resource_bucket_key_prefix: str,
- resource_bucket_refresh_frequency_min: str,
- metrics_url: str,
- send_anonymous_usage: str,
- send_anonymous_usage_condition: CfnCondition,
- ) -> None:
- super().__init__(scope, construct_id)
-
- aws_ssm.StringParameter(
- self,
- "ssm-cms-deployment-uuid",
- string_value=deployment_uuid,
- description="Solution UUID used to tag resources within CMS",
- parameter_name=f"/{CmsConstants.STAGE}/cms/common/config/deployment-uuid",
- )
-
- aws_ssm.StringParameter(
- self,
- "ssm-cms-resource-bucket",
- string_value=resource_bucket,
- description="Bucket name where CMS Resources are to be accessed from",
- parameter_name=f"/{CmsConstants.STAGE}/common/config/cms-resource-bucket/name",
- )
-
- aws_ssm.StringParameter(
- self,
- "ssm-cms-resource-bucket-region",
- string_value=resource_bucket_region,
- description="Bucket region where CMS Resources are to be accessed from",
- parameter_name=f"/{CmsConstants.STAGE}/common/config/cms-resource-bucket/region",
- )
-
- aws_ssm.StringParameter(
- self,
- "ssm-cms-resource-bucket-backstage-template-key-prefix",
- string_value=resource_bucket_key_prefix,
- description="Bucket key prefix where CMS Resources are to be accessed from",
- parameter_name=f"/{CmsConstants.STAGE}/common/config/cms-resource-bucket/template-key-prefix",
- )
-
- aws_ssm.StringParameter(
- self,
- "ssm-cms-resource-bucket-backstage-template-refresh-freq-mins",
- string_value=resource_bucket_refresh_frequency_min,
- description="Frequency to allow refresh of backstage templates",
- parameter_name=f"/{CmsConstants.STAGE}/common/config/cms-resource-bucket/refresh-frequency-mins",
- )
-
- aws_ssm.StringParameter(
- self,
- "cms-metrics-reporting-enabled",
- string_value=send_anonymous_usage,
- description="Anonymous metrics reporting enabled state",
- parameter_name=f"/{CmsConstants.STAGE}/common/metrics/enabled",
- )
-
- metrics_parameter = aws_ssm.StringParameter(
- self,
- "cms-metrics-reporting-url",
- string_value=metrics_url,
- description="Anonymous metrics reporting url",
- parameter_name=f"/{CmsConstants.STAGE}/common/metrics/url",
- )
-
- metrics_cfn_resource: CfnResource = cast(
- CfnResource, metrics_parameter.node.default_child
- )
- metrics_cfn_resource.cfn_options.condition = send_anonymous_usage_condition
diff --git a/source/infrastructure/handlers/__init__.py b/source/infrastructure/handlers/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/infrastructure/handlers/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/infrastructure/handlers/custom_resource/__init__.py b/source/infrastructure/handlers/custom_resource/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/infrastructure/handlers/custom_resource/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/infrastructure/handlers/custom_resource/custom_resource.py b/source/infrastructure/handlers/custom_resource/custom_resource.py
deleted file mode 100644
index a2efdc4c..00000000
--- a/source/infrastructure/handlers/custom_resource/custom_resource.py
+++ /dev/null
@@ -1,310 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-import json
-import os
-import time
-import uuid
-from enum import Enum
-from functools import lru_cache
-from typing import TYPE_CHECKING, Any, Dict, Iterator, Tuple
-
-# Third Party Libraries
-import boto3
-import requests
-from aws_lambda_powertools import Logger, Tracer
-from aws_lambda_powertools.utilities.typing import LambdaContext
-from botocore.config import Config
-from botocore.exceptions import ClientError
-
-tracer = Tracer()
-logger = Logger()
-
-if TYPE_CHECKING:
- # Third Party Libraries
- from mypy_boto3_proton import ProtonClient
- from mypy_boto3_s3 import S3Client
-
-else:
- S3Client = object
- ProtonClient = object
-
-REMAINING_TIME_THRESHOLD = 10000 # milliseconds
-
-
-@lru_cache(maxsize=128)
-def get_s3_client() -> S3Client:
- return boto3.client(
- "s3", config=Config(user_agent_extra=os.environ["USER_AGENT_STRING"])
- )
-
-
-@lru_cache(maxsize=128)
-def get_proton_client() -> ProtonClient:
- return boto3.client(
- "proton", config=Config(user_agent_extra=os.environ["USER_AGENT_STRING"])
- )
-
-
-@logger.inject_lambda_context
-@tracer.capture_lambda_handler
-def handler(event: Dict[str, Any], context: LambdaContext) -> Dict[str, Any]:
- response = {"Status": CustomResourceTypes.StatusTypes.SUCCESS.value, "Data": {}}
- reason = f"See the details in CloudWatch Log Stream: {context.log_stream_name}"
-
- try:
-
- match event["ResourceProperties"]["Resource"]:
- case CustomResourceTypes.ResourceTypes.CREATE_PROTON_ENVIRONMENT.value:
- create_proton_environment(event, context)
- case CustomResourceTypes.ResourceTypes.CREATE_DEPLOYMENT_UUID.value:
- response["Data"] = create_deployment_uuid(event)
- case _:
- raise KeyError(
- f"No Custom Resource Type: {event['ResourceProperties']['Resource']}"
- )
-
- except Exception as exception: # pylint: disable=W0703
- # Wrap all exceptions so CloudFormation doesn't hang
- logger.error("CustomResource error: %s", str(exception), exc_info=True)
- response["Status"] = CustomResourceTypes.StatusTypes.FAILED.value
- reason = f"{str(exception)} ... {reason}"
-
- send_cloud_formation_response(
- event,
- response,
- reason,
- )
-
- return response
-
-
-@tracer.capture_method
-def send_cloud_formation_response(
- event: Dict[str, Any], response: Dict[str, Any], reason: str
-) -> None:
- response_body = {
- "Status": response["Status"],
- "Reason": reason,
- "PhysicalResourceId": event["LogicalResourceId"],
- "StackId": event["StackId"],
- "RequestId": event["RequestId"],
- "LogicalResourceId": event["LogicalResourceId"],
- "Data": response["Data"],
- }
-
- logger.info("response", extra={"response_body": response_body})
-
- headers = {"Content-Type": "application/json"}
-
- requests.put(
- event["ResponseURL"],
- data=json.dumps(response_body),
- headers=headers,
- timeout=60,
- )
-
-
-@tracer.capture_method
-def _get_s3_proton_templates(
- s3_bucket_name: str, s3_template_path_prefix: str
-) -> Iterator[Tuple[str, str]]:
- # Add a trailing slash to the key prefix if it's not already included
- s3_template_prefix = os.path.join(s3_template_path_prefix, "")
-
- s3_response = get_s3_client().list_objects_v2(
- Bucket=s3_bucket_name,
- Prefix=s3_template_prefix,
- StartAfter=s3_template_prefix,
- )
-
- templates = s3_response.get("Contents", [])
-
- for template in templates:
- template_key = template["Key"]
-
- template_path_split = template_key.split(".tar.gz")
- if len(template_path_split) != 2:
- logger.warning("Skipping not .tar.gz object: %s", template_key)
- continue
-
- template_proton_name = os.path.basename(template_path_split[0])
-
- yield template_key, template_proton_name
-
-
-@tracer.capture_method
-def _create_proton_environment_template(
- template_proton_name: str, bucket_name: str, template_key: str
-) -> Tuple[str, str]:
-
- get_proton_client().create_environment_template(name=template_proton_name)
-
- new_proton_env_template_version = (
- get_proton_client().create_environment_template_version(
- source={
- "s3": {
- "bucket": bucket_name,
- "key": template_key,
- }
- },
- templateName=template_proton_name,
- majorVersion="1",
- )
- )
-
- environment_template_version = new_proton_env_template_version[
- "environmentTemplateVersion"
- ]
- major_version = environment_template_version["majorVersion"]
- minor_version = environment_template_version["minorVersion"]
-
- return major_version, minor_version
-
-
-@tracer.capture_method
-def _wait_for_proton_environment_template_to_be_ready(
- context: LambdaContext,
- template_proton_name: str,
- template_major_version: str,
- template_minor_version: str,
-) -> None:
-
- while context.get_remaining_time_in_millis() > REMAINING_TIME_THRESHOLD:
-
- logger.info("Waiting for template draft")
-
- if get_proton_client().get_environment_template_version(
- templateName=template_proton_name,
- majorVersion=template_major_version,
- minorVersion=template_minor_version,
- )["environmentTemplateVersion"]["status"] in ["DRAFT", "PUBLISHED"]:
- return
-
- time.sleep(5)
-
- logger.error("Lambda timeout margin exceeded, gracefully failing before shutdown.")
- raise TimeoutError(
- "Proton template version could not be created in DRAFT/PUBLISHED state before CFN Custom Resource timeout."
- )
-
-
-@tracer.capture_method
-def _create_or_update_proton_environment(
- template_proton_name: str,
- template_major_version: str,
- template_minor_version: str,
- codebuild_iam_role_arn: str,
-) -> None:
- try:
-
- current_environment = get_proton_client().get_environment(
- name=template_proton_name
- )
-
- current_environment_template_name = current_environment["environment"][
- "templateName"
- ]
- if current_environment_template_name != template_proton_name:
- raise ValueError(
- f"Unexpected Proton Template '{current_environment_template_name}' found on Existing Environment. Expecting template: '{template_proton_name}'"
- )
-
- get_proton_client().update_environment(
- name=template_proton_name,
- templateMajorVersion=template_major_version,
- templateMinorVersion=template_minor_version,
- deploymentType="MINOR_VERSION",
- spec="{proton: EnvironmentSpec, spec: {a_number: 123}}",
- codebuildRoleArn=codebuild_iam_role_arn,
- )
- except get_proton_client().exceptions.ResourceNotFoundException:
- get_proton_client().create_environment(
- name=template_proton_name,
- templateName=template_proton_name,
- templateMajorVersion=template_major_version,
- templateMinorVersion=template_minor_version,
- spec="{proton: EnvironmentSpec, spec: {a_number: 123}}",
- codebuildRoleArn=codebuild_iam_role_arn,
- )
-
-
-@tracer.capture_method
-def create_proton_environment(event: Dict[str, Any], context: LambdaContext) -> None:
- if event["RequestType"] in [
- CustomResourceTypes.RequestTypes.CREATE.value,
- CustomResourceTypes.RequestTypes.UPDATE.value,
- ]:
- print(event)
- try:
-
- bucket_name = event["ResourceProperties"].get("TEMPLATE_S3_BUCKET_NAME")
- codebuild_iam_role_arn = event["ResourceProperties"].get(
- "CODE_BUILD_IAM_ROLE"
- )
-
- templates = _get_s3_proton_templates(
- s3_bucket_name=bucket_name,
- s3_template_path_prefix=event["ResourceProperties"].get(
- "TEMPLATE_S3_KEY_PREFIX"
- ),
- )
-
- for template_key, template_proton_name in templates:
-
- major_version, minor_version = _create_proton_environment_template(
- template_proton_name=template_proton_name,
- bucket_name=bucket_name,
- template_key=template_key,
- )
-
- _wait_for_proton_environment_template_to_be_ready(
- context=context,
- template_proton_name=template_proton_name,
- template_major_version=major_version,
- template_minor_version=minor_version,
- )
-
- get_proton_client().update_environment_template_version(
- templateName=template_proton_name,
- majorVersion=major_version,
- minorVersion=minor_version,
- status="PUBLISHED",
- )
-
- _create_or_update_proton_environment(
- template_proton_name=template_proton_name,
- template_major_version=major_version,
- template_minor_version=minor_version,
- codebuild_iam_role_arn=codebuild_iam_role_arn,
- )
-
- except ClientError as error:
- logger.error("Error while creating environment: %s", error)
-
-
-@tracer.capture_method
-def create_deployment_uuid(event: Dict[str, Any]) -> Dict[str, Any]:
- response = {}
-
- if event["RequestType"] == CustomResourceTypes.RequestTypes.CREATE.value:
- response["SolutionUUID"] = str(uuid.uuid4())
-
- return response
-
-
-class CustomResourceTypes:
- class RequestTypes(Enum):
- CREATE = "Create"
- DELETE = "Delete"
- UPDATE = "Update"
-
- class ResourceTypes(Enum):
- CREATE_PROTON_ENVIRONMENT = "CreateProtonEnvironment"
- CREATE_DEPLOYMENT_UUID = "CreateDeploymentUUID"
-
- class StatusTypes(Enum):
- SUCCESS = "SUCCESS"
- FAILED = "FAILED"
diff --git a/source/infrastructure/handlers/metrics/app/__init__.py b/source/infrastructure/handlers/metrics/app/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/infrastructure/handlers/metrics/app/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/infrastructure/handlers/metrics/app/lib/__init__.py b/source/infrastructure/handlers/metrics/app/lib/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/infrastructure/handlers/metrics/app/lib/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/infrastructure/handlers/metrics/app/main.py b/source/infrastructure/handlers/metrics/app/main.py
deleted file mode 100644
index c240ea3c..00000000
--- a/source/infrastructure/handlers/metrics/app/main.py
+++ /dev/null
@@ -1,113 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-import datetime
-import os
-from functools import lru_cache
-from typing import Any, Dict
-
-# Third Party Libraries
-import boto3
-from aws_lambda_powertools import Logger, Tracer
-from aws_lambda_powertools.utilities.typing import LambdaContext
-from botocore.config import Config
-
-# Connected Mobility Solution on AWS
-from .lib import data_firehose_helper, metrics_publish, s3_helper
-
-tracer = Tracer()
-logger = Logger()
-
-
-@lru_cache(maxsize=128)
-def get_resourcegroupstaggingapi_client() -> Any:
- return boto3.client(
- "resourcegroupstaggingapi",
- config=Config(user_agent_extra=os.environ["USER_AGENT_STRING"]),
- )
-
-
-@lru_cache(maxsize=128)
-def get_cloudwatch_client() -> Any:
- return boto3.client(
- "cloudwatch", config=Config(user_agent_extra=os.environ["USER_AGENT_STRING"])
- )
-
-
-@tracer.capture_lambda_handler
-def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> None:
-
- s3_storage_bytes = None
- data_firehose_metrics = None
-
- resourcegroupstaggingapi = get_resourcegroupstaggingapi_client()
- cloudwatch = get_cloudwatch_client()
-
- config = build_config()
-
- try:
-
- s3_storage_bytes = s3_helper.get_cms_s3_total_storage_in_use(
- config, resourcegroupstaggingapi, cloudwatch
- )
-
- except Exception as err: # pylint: disable=W0718
- logger.error("Failed to record S3 metrics")
- logger.error(err)
-
- try:
- data_firehose_metrics = data_firehose_helper.get_cms_data_firehose_utilization(
- config, resourcegroupstaggingapi, cloudwatch
- )
-
- except Exception as err: # pylint: disable=W0718
- logger.error("Failed to record Data Firehose metrics")
- logger.error(err)
-
- try:
-
- metric: Dict[str, Any] = {
- "Type": "CMSDeploymentMetricScrape",
- }
-
- if data_firehose_metrics:
- metric["DailyNumberOfDeliveryStreamsUsed"] = data_firehose_metrics[
- "total_num_data_streams_in_use_on_day"
- ]
- metric["DailyIncomingPutRequests"] = data_firehose_metrics[
- "total_put_requests_per_day"
- ]
-
- if s3_storage_bytes:
- metric["SumAllBucketsSizeBytes"] = s3_storage_bytes
-
- metrics_publish.write_metric(config, metric, config["metric_timestamp"])
- except Exception as err: # pylint: disable=W0718
- logger.error("Failed to publish metrics")
- logger.error(err)
-
-
-def build_config() -> Dict[str, Any]:
- config: Dict[str, Any] = {}
-
- utc_today = datetime.datetime.utcnow().date()
-
- config["today"] = datetime.datetime(
- utc_today.year,
- utc_today.month,
- utc_today.day,
- tzinfo=datetime.timezone.utc,
- )
- config["yesterday"] = config["today"] - datetime.timedelta(days=1)
- config["metric_timestamp"] = datetime.datetime.now()
-
- config["solution_id"] = os.environ["SOLUTION_ID"]
- config["solution_version"] = os.environ["SOLUTION_VERSION"]
- config["account_id"] = os.environ["AWS_ACCOUNT_ID"]
- config["region"] = os.environ["AWS_REGION"]
- config["metrics_solution_url"] = os.environ["METRICS_SOLUTION_URL"]
- config["deployment_uuid"] = os.environ["DEPLOYMENT_UUID"]
-
- return config
diff --git a/source/infrastructure/handlers/metrics/app/tests/__init__.py b/source/infrastructure/handlers/metrics/app/tests/__init__.py
deleted file mode 100644
index b6a5eed0..00000000
--- a/source/infrastructure/handlers/metrics/app/tests/__init__.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-import datetime
-import os
-import unittest
-from typing import Any, Dict, List
-
-__all__: List[Any] = []
-
-
-class UnitTestCommon(unittest.TestCase):
- def setUp(self) -> None:
- set_common_env_variables()
- return super().setUp()
-
-
-def set_common_env_variables() -> None:
- os.environ["SOLUTION_ID"] = "SO0241"
- os.environ["SOLUTION_VERSION"] = "v1.0.4"
- os.environ["AWS_ACCOUNT_ID"] = "0123456789123"
- os.environ["AWS_REGION"] = "us-east-1"
- os.environ["DEPLOYMENT_UUID"] = "DUMMY"
- os.environ["METRICS_SOLUTION_URL"] = "https://localhost"
- os.environ["USER_AGENT_STRING"] = "USER_AGENT"
-
-
-def get_solution_resource_tags(
- solution_id: str, deployment_uuid: str, module_name: str
-) -> List[Dict[str, Any]]:
- return [
- {"Key": "Solutions:SolutionID", "Value": solution_id},
- {
- "Key": "Solutions:ModuleName",
- "Value": module_name,
- },
- {"Key": "Solutions:DeploymentUUID", "Value": deployment_uuid},
- {"Key": "Solutions:SolutionVersion", "Value": "v1.0.4"},
- {"Key": "Solutions:ApplicationType", "Value": "AWS-Solutions"},
- {
- "Key": "Solutions:SolutionName",
- "Value": "Connected Mobility Solution on AWS",
- },
- ]
-
-
-def get_halfway_yesterday_time_utc() -> datetime.datetime:
- utc_today = datetime.datetime.utcnow().date()
- utc_today_time = datetime.datetime(
- utc_today.year,
- utc_today.month,
- utc_today.day,
- 0,
- 0,
- 0,
- 0,
- datetime.timezone.utc,
- )
- return utc_today_time - datetime.timedelta(hours=12)
diff --git a/source/infrastructure/handlers/metrics/app/tests/lib/__init__.py b/source/infrastructure/handlers/metrics/app/tests/lib/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/infrastructure/handlers/metrics/app/tests/lib/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/infrastructure/handlers/metrics/app/tests/test_main.py b/source/infrastructure/handlers/metrics/app/tests/test_main.py
deleted file mode 100644
index 32f526de..00000000
--- a/source/infrastructure/handlers/metrics/app/tests/test_main.py
+++ /dev/null
@@ -1,113 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-import datetime
-import os
-from typing import Any, Dict
-from unittest.mock import MagicMock, patch
-
-# Third Party Libraries
-import boto3
-from freezegun import freeze_time
-
-# Connected Mobility Solution on AWS
-from ..lib import data_firehose_helper, metrics_publish, s3_helper
-from ..main import build_config, lambda_handler
-from . import UnitTestCommon
-
-TEST_YEAR = 2023
-TEST_MONTH = 1
-TEST_DAY = 7
-TEST_HOUR = 7
-TEST_MINUTE = 0
-TEST_SECOND = 0
-TEST_TZ_OFFSET = -5
-
-
-class TestHandler(UnitTestCommon):
- @freeze_time(
- f"{TEST_YEAR}-{TEST_MONTH}-{TEST_DAY} {TEST_HOUR}:{TEST_MINUTE}:{TEST_SECOND}",
- tz_offset=TEST_TZ_OFFSET,
- )
- @patch.object(metrics_publish, "write_metric")
- @patch.object(data_firehose_helper, "get_cms_data_firehose_utilization")
- @patch.object(s3_helper, "get_cms_s3_total_storage_in_use")
- @patch.object(boto3, "client")
- def test_get_metrics(
- self,
- mock_boto3_client: MagicMock,
- mock_get_cms_s3_total_storage_in_use: MagicMock,
- mock_get_cms_data_firehose_utilization: MagicMock,
- mock_write_metric: MagicMock,
- ) -> None:
- mock_get_cms_s3_total_storage_in_use.return_value = 1234
- mock_get_cms_data_firehose_utilization.return_value = {
- "total_put_requests_per_day": 5678,
- "total_num_data_streams_in_use_on_day": 1,
- }
-
- event: Dict[str, Any] = {}
- context: Dict[str, Any] = {}
-
- lambda_handler(event, context)
-
- self.assertEqual(mock_get_cms_s3_total_storage_in_use.call_count, 1)
- self.assertEqual(mock_get_cms_data_firehose_utilization.call_count, 1)
- self.assertEqual(mock_write_metric.call_count, 1)
-
- self.assertEqual(
- mock_get_cms_s3_total_storage_in_use.call_args[0][0],
- {
- "today": datetime.datetime(
- TEST_YEAR, TEST_MONTH, TEST_DAY, 0, 0, tzinfo=datetime.timezone.utc
- ),
- "yesterday": datetime.datetime(
- TEST_YEAR,
- TEST_MONTH,
- TEST_DAY - 1,
- 0,
- 0,
- tzinfo=datetime.timezone.utc,
- ),
- "metric_timestamp": datetime.datetime(
- TEST_YEAR,
- TEST_MONTH,
- TEST_DAY,
- TEST_HOUR,
- TEST_MINUTE,
- TEST_SECOND,
- 0,
- )
- + datetime.timedelta(hours=TEST_TZ_OFFSET),
- "solution_id": os.environ["SOLUTION_ID"],
- "solution_version": os.environ["SOLUTION_VERSION"],
- "account_id": os.environ["AWS_ACCOUNT_ID"],
- "region": os.environ["AWS_REGION"],
- "metrics_solution_url": os.environ["METRICS_SOLUTION_URL"],
- "deployment_uuid": os.environ["DEPLOYMENT_UUID"],
- },
- )
-
- @freeze_time(
- f"{TEST_YEAR}-{TEST_MONTH}-{TEST_DAY} {TEST_HOUR}:{TEST_MINUTE}:{TEST_SECOND}",
- tz_offset=TEST_TZ_OFFSET,
- )
- def test_build_config(self) -> None:
- config = build_config()
-
- self.assertEqual(config["today"].day, datetime.datetime.utcnow().date().day)
-
- self.assertEqual(
- config["yesterday"].day, datetime.datetime.utcnow().date().day - 1
- )
-
- self.assertEqual(config["solution_id"], os.environ["SOLUTION_ID"])
- self.assertEqual(config["solution_version"], os.environ["SOLUTION_VERSION"])
- self.assertEqual(config["account_id"], os.environ["AWS_ACCOUNT_ID"])
- self.assertEqual(config["region"], os.environ["AWS_REGION"])
- self.assertEqual(
- config["metrics_solution_url"], os.environ["METRICS_SOLUTION_URL"]
- )
- self.assertEqual(config["deployment_uuid"], os.environ["DEPLOYMENT_UUID"])
diff --git a/source/infrastructure/stacks/__init__.py b/source/infrastructure/stacks/__init__.py
deleted file mode 100644
index c018c824..00000000
--- a/source/infrastructure/stacks/__init__.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-import os
-from dataclasses import dataclass
-
-# Third Party Libraries
-from aws_cdk import ArnFormat, Stack, aws_iam
-from constructs import Construct
-
-
-# pylint: disable=invalid-name
-@dataclass(frozen=True)
-class CmsConstantsClass:
- STAGE: str = os.environ.get("STAGE", "dev")
- APP_NAME: str = "cms"
- STACK_NAME: str = f"cms-{STAGE}"
- MODULE_NAME: str = f"Connected-mobility-solution-on-aws-{STAGE}"
- SOLUTION_NAME: str = "Connected Mobility Solution on AWS"
- SOLUTION_ID: str = "SO0241"
- SOLUTION_VERSION: str = "v1.0.4"
- APPLICATION_TYPE: str = "AWS-Solutions"
- USER_AGENT_STRING: str = f"AWSSOLUTION/{SOLUTION_ID}/{SOLUTION_VERSION}"
-
-
-def generate_lambda_cloudwatch_logs_policy_document(
- self: Construct, lambda_function_name: str
-) -> aws_iam.PolicyDocument:
- return aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "logs:CreateLogGroup",
- "logs:CreateLogStream",
- "logs:PutLogEvents",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="logs",
- resource="log-group",
- resource_name=f"/aws/lambda/{lambda_function_name}",
- arn_format=ArnFormat.COLON_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="logs",
- resource="log-group",
- resource_name=f"/aws/lambda/{lambda_function_name}:log-stream:*",
- arn_format=ArnFormat.COLON_RESOURCE_NAME,
- ),
- ],
- ),
- ]
- )
-
-
-CmsConstants = CmsConstantsClass()
diff --git a/source/infrastructure/stacks/cms_stack.py b/source/infrastructure/stacks/cms_stack.py
deleted file mode 100644
index 8081d509..00000000
--- a/source/infrastructure/stacks/cms_stack.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any
-
-# Third Party Libraries
-from aws_cdk import Aspects, CfnCondition, CfnMapping, Fn, Stack, Tags
-from constructs import Construct
-from source.infrastructure.aspects.condition_aspect import ConditionAspect
-
-# Connected Mobility Solution on AWS
-from ..constructs.app_registry import AppRegistryConstruct
-from ..constructs.custom_resource_lambda import CustomResourceLambdaConstruct
-from ..constructs.deployment_uuid_construct import DeploymentUUIDConstruct
-from ..constructs.lambda_dependencies import LambdaDependenciesConstruct
-from ..constructs.module_integration import ModuleOutputsConstruct
-from ..stacks import CmsConstants
-from .components.metrics import Metrics
-from .components.pipelines import Pipelines
-from .components.proton_environment import ProtonEnvironment
-
-
-class CmsStack(Stack):
- def __init__( # pylint: disable=too-many-locals
- self, scope: Construct, stack_id: str, **kwargs: Any
- ) -> None:
- super().__init__(scope, stack_id, **kwargs)
-
- solution_mapping = CfnMapping(
- self,
- "Solution",
- mapping={
- "Config": {
- "SendAnonymousUsage": "Yes",
- }
- },
- )
-
- send_anonymous_usage = solution_mapping.find_in_map(
- "Config", "SendAnonymousUsage"
- )
-
- send_anonymous_usage_condition = CfnCondition(
- self,
- "SendAnonymousUsage",
- expression=Fn.condition_equals(send_anonymous_usage, "Yes"),
- )
-
- metrics_url = "https://metrics.awssolutionsbuilder.com/generic"
-
- dependency_layer = LambdaDependenciesConstruct(
- self,
- "cms-dependency-layer",
- dependency_layer_dir_name="cmdp_dependency_layer",
- )
- custom_resource_construct = CustomResourceLambdaConstruct(
- self,
- "cms-custom-resource",
- dependency_layer=dependency_layer.dependency_layer,
- )
-
- deployment_uuid_construct = DeploymentUUIDConstruct(
- self,
- "cms-deployment-uuid",
- custom_resource_lambda_arn=custom_resource_construct.custom_resource_lambda.function_arn,
- )
- deployment_uuid = (
- deployment_uuid_construct.deployment_uuid_custom_resource.get_att_string(
- "SolutionUUID"
- )
- )
-
- app_registry = AppRegistryConstruct(
- self,
- "cms-app-registry",
- application_name=CmsConstants.APP_NAME,
- application_type=CmsConstants.APPLICATION_TYPE,
- solution_id=CmsConstants.SOLUTION_ID,
- solution_name=CmsConstants.SOLUTION_NAME,
- solution_version=CmsConstants.SOLUTION_VERSION,
- )
-
- proton_environment = ProtonEnvironment(
- self,
- "cms-proton-environment",
- custom_resource_construct,
- )
- pipelines = Pipelines(
- self,
- "cms-pipelines",
- )
-
- metrics_construct = Metrics(
- self,
- "cms-metrics",
- metrics_url,
- deployment_uuid,
- )
-
- Aspects.of(metrics_construct).add(
- ConditionAspect(send_anonymous_usage_condition)
- )
-
- module_outputs = ModuleOutputsConstruct(
- self,
- "cms-module-outputs",
- deployment_uuid=deployment_uuid,
- resource_bucket=self.node.get_context("cms-resource-bucket"),
- resource_bucket_region=self.node.get_context("cms-resource-bucket-region"),
- resource_bucket_key_prefix=self.node.get_context(
- "cms-resource-bucket-backstage-template-key-prefix"
- ),
- resource_bucket_refresh_frequency_min=self.node.get_context(
- "cms-resource-bucket-backstage-refresh-frequency-mins"
- ),
- metrics_url=metrics_url,
- send_anonymous_usage=send_anonymous_usage,
- send_anonymous_usage_condition=send_anonymous_usage_condition,
- )
-
- Tags.of(app_registry).add("Solutions:DeploymentUUID", deployment_uuid)
- Tags.of(proton_environment).add("Solutions:DeploymentUUID", deployment_uuid)
- Tags.of(pipelines).add("Solutions:DeploymentUUID", deployment_uuid)
- Tags.of(module_outputs).add("Solutions:DeploymentUUID", deployment_uuid)
diff --git a/source/infrastructure/stacks/components/__init__.py b/source/infrastructure/stacks/components/__init__.py
deleted file mode 100644
index b0f6a41f..00000000
--- a/source/infrastructure/stacks/components/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from typing import Any, List
-
-__all__: List[Any] = []
diff --git a/source/infrastructure/stacks/components/metrics.py b/source/infrastructure/stacks/components/metrics.py
deleted file mode 100644
index aa45a7e1..00000000
--- a/source/infrastructure/stacks/components/metrics.py
+++ /dev/null
@@ -1,182 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-import os
-from os.path import abspath, dirname
-from pathlib import Path
-from typing import Any
-
-# Third Party Libraries
-import toml
-from aws_cdk import (
- Duration,
- RemovalPolicy,
- Stack,
- aws_events,
- aws_events_targets,
- aws_iam,
- aws_lambda,
- aws_logs,
-)
-from constructs import Construct
-
-# Connected Mobility Solution on AWS
-from ...stacks import CmsConstants, generate_lambda_cloudwatch_logs_policy_document
-
-
-class Metrics(Construct):
- def __init__( # pylint: disable=too-many-locals
- self,
- scope: Stack,
- stack_id: str,
- metrics_url: str,
- deployment_uuid: str,
- **kwargs: Any,
- ) -> None:
- super().__init__(scope, stack_id, **kwargs)
-
- metrics_lambda_name = f"{CmsConstants.STACK_NAME}-anonymous-metrics-reporting"
-
- metrics_lambda_role = aws_iam.Role(
- self,
- "metrics-reporting-lambda-role",
- assumed_by=aws_iam.ServicePrincipal("lambda.amazonaws.com"),
- inline_policies={
- "lambda-logs-policy": generate_lambda_cloudwatch_logs_policy_document(
- self, metrics_lambda_name
- ),
- "cloudwatch-metrics-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "cloudwatch:GetMetricData",
- "cloudwatch:GetMetricStatistics",
- "cloudwatch:ListMetrics",
- ],
- resources=[
- "*"
- ], # cloudwatch:Get*/List* does not support any kind of access control (https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazoncloudwatch.html)
- )
- ]
- ),
- "resourcegroupstaggingapi-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "tag:GetResources",
- "tag:GetTagKeys",
- "tag:GetTagValues",
- ],
- resources=["*"],
- )
- ]
- ),
- },
- )
-
- dependency_layer_name = "cms_metrics_dependency_layer"
-
- metrics_function = aws_lambda.Function(
- self,
- "cmdp-metrics-lambda",
- code=aws_lambda.Code.from_asset(
- "source/infrastructure/handlers/metrics", exclude=["**/tests/*"]
- ),
- handler="app.main.lambda_handler", # Must place in nested folder to resolve lambda relative import issue: https://gist.github.com/gene1wood/06a64ba80cf3fe886053f0ca6d375bc0
- function_name=metrics_lambda_name,
- role=metrics_lambda_role,
- runtime=aws_lambda.Runtime.PYTHON_3_10,
- timeout=Duration.seconds(300),
- layers=[
- self.package_dependency_layer(
- dir_path=f"{os.getcwd()}/{self.node.try_get_context('app-location')}/{dependency_layer_name}",
- dependency_layer_name=dependency_layer_name,
- ),
- ],
- environment={
- "USER_AGENT_STRING": CmsConstants.USER_AGENT_STRING,
- "SOLUTION_ID": CmsConstants.SOLUTION_ID,
- "SOLUTION_VERSION": CmsConstants.SOLUTION_VERSION,
- "AWS_ACCOUNT_ID": Stack.of(self).account,
- "DEPLOYMENT_UUID": deployment_uuid,
- "METRICS_SOLUTION_URL": metrics_url,
- },
- log_retention=aws_logs.RetentionDays.THREE_MONTHS,
- )
-
- event_rule = aws_events.Rule(
- self,
- "metrics-lambda-cron-rule",
- schedule=aws_events.Schedule.cron(hour="1", minute="0"),
- )
-
- event_rule.add_target(
- target=aws_events_targets.LambdaFunction(metrics_function)
- )
-
- def package_dependency_layer(
- self, dir_path: str, dependency_layer_name: str
- ) -> aws_lambda.LayerVersion:
- source_pipfile = f"{dirname(dirname(dirname(abspath(__file__))))}/../../Pipfile"
- pip_path = f"{dir_path}/python"
-
- # Create the folders out to the build directory
- Path(pip_path).mkdir(parents=True, exist_ok=True)
- requirements = f"{dir_path}/requirements.txt"
- exclude_list = ["chalice", "aws-cdk-lib", "boto3"]
- # Copy Pipfile to build directory as requirements.txt format and excluding the large packages
- with open(source_pipfile, "r", encoding="utf-8") as pipfile:
- new_pipfile = toml.load(pipfile)
- with open(requirements, "w", encoding="utf-8") as req_file:
-
- def req_formatter(package: str, constraint: Any) -> None:
- if constraint == "*":
- req_file.write(package + "\n")
- return
-
- try:
- extras = (
- str(constraint.get("extras", "all"))
- .replace("'", "")
- .replace('"', "")
- )
- version = (
- constraint["version"] if constraint["version"] != "*" else ""
- )
- req_file.write(f"{package}{extras} {version}\n")
- except (TypeError, KeyError, AttributeError):
- if isinstance(constraint, str):
- req_file.write(f"{package} {constraint}\n")
-
- for package, constraint in new_pipfile["packages"].items():
- if package not in exclude_list:
- req_formatter(package, constraint)
-
- # Install the requirements in the build directory (CDK will use this whole folder to build the zip)
- os.system( # nosec
- f"/bin/bash -c 'python -m pip install -q --upgrade --target {pip_path} --requirement {requirements}'"
- )
-
- dependency_layer = aws_lambda.LayerVersion(
- self,
- "metrics-dependency-layer-version",
- removal_policy=RemovalPolicy.DESTROY,
- code=aws_lambda.Code.from_asset(
- f"{os.getcwd()}/{self.node.try_get_context('app-location')}/{dependency_layer_name}"
- ),
- compatible_architectures=[
- aws_lambda.Architecture.X86_64,
- aws_lambda.Architecture.ARM_64,
- ],
- compatible_runtimes=[
- aws_lambda.Runtime.PYTHON_3_8,
- aws_lambda.Runtime.PYTHON_3_9,
- aws_lambda.Runtime.PYTHON_3_10,
- ],
- )
-
- return dependency_layer
diff --git a/source/infrastructure/stacks/components/pipelines.py b/source/infrastructure/stacks/components/pipelines.py
deleted file mode 100644
index 70e0ee89..00000000
--- a/source/infrastructure/stacks/components/pipelines.py
+++ /dev/null
@@ -1,690 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-from functools import partial
-from typing import Any, Optional, Union
-
-# Third Party Libraries
-from aws_cdk import (
- ArnFormat,
- CfnParameter,
- RemovalPolicy,
- Stack,
- aws_chatbot,
- aws_codebuild,
- aws_codepipeline,
- aws_codepipeline_actions,
- aws_ec2,
- aws_ecr,
- aws_iam,
- aws_kms,
- aws_logs,
- aws_s3_assets,
- aws_secretsmanager,
- aws_ssm,
- pipelines,
-)
-from constructs import Construct
-
-# Connected Mobility Solution on AWS
-from ...stacks import CmsConstants
-
-
-class Pipelines(Construct):
- def __init__( # pylint: disable=too-many-locals
- self, scope: Stack, stack_id: str, **kwargs: Any
- ) -> None:
- super().__init__(scope, stack_id, **kwargs)
-
- try:
- slack_chatbot_arn = CfnParameter(
- self,
- "slack-chatbot-arn",
- type="String",
- description="The Slack Chatbot ARN to send notifications to during CodePipeline stages",
- default=self.node.get_context("chatbot-configuration-arn"),
- )
-
- add_slack_chatbot = partial(
- self.add_slack_notification_codepipeline,
- True,
- slack_chatbot_arn.value_as_string,
- )
- except RuntimeError:
- print(
- "WARNING: Slack Chatbot ARN is not set. Notifications will not be setup."
- )
- add_slack_chatbot = partial(
- self.add_slack_notification_codepipeline, False, ""
- )
-
- backend_secret_object = aws_secretsmanager.Secret( # nosec[CWE-259]
- self,
- "backend-secret",
- description="Backend secret",
- secret_name=f"{CmsConstants.STACK_NAME}/backend-secret",
- generate_secret_string=aws_secretsmanager.SecretStringGenerator(),
- )
-
- # Add rotation to these secrets
- # https://github.com/aws-samples/aws-secrets-manager-rotation-lambdas/blob/master/SecretsManagerRotationTemplate/lambda_function.py
- # https://gist.github.com/StevenACoffman/f0c084b428977430d2baacd0263c3563
-
- vpc = aws_ec2.Vpc(
- self,
- "cms-vpc",
- vpc_name=f"{CmsConstants.STACK_NAME}-vpc",
- ip_addresses=aws_ec2.IpAddresses.cidr(
- self.node.get_context("vpc-cidr-range")
- ),
- availability_zones=Stack.of(self).availability_zones,
- subnet_configuration=[
- aws_ec2.SubnetConfiguration(
- name="application",
- subnet_type=aws_ec2.SubnetType.PRIVATE_WITH_EGRESS,
- ),
- aws_ec2.SubnetConfiguration(
- name="private", subnet_type=aws_ec2.SubnetType.PRIVATE_ISOLATED
- ),
- aws_ec2.SubnetConfiguration(
- name="public", subnet_type=aws_ec2.SubnetType.PUBLIC
- ),
- ],
- nat_gateways=1,
- )
-
- vpc_log_group_kms_key = aws_kms.Key(
- self,
- "vpc-log-group-kms-key",
- alias="vpc-log-group-kms-key",
- enable_key_rotation=True,
- )
-
- vpc_log_group = aws_logs.LogGroup(
- self,
- "cms-vpc-log-group",
- removal_policy=RemovalPolicy.RETAIN,
- retention=aws_logs.RetentionDays.THREE_MONTHS,
- )
-
- vpc_log_group_kms_key.add_to_resource_policy(
- statement=aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- principals=[
- aws_iam.ServicePrincipal(
- f"logs.{Stack.of(self).region}.amazonaws.com"
- )
- ],
- actions=["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
- resources=["*"],
- )
- )
-
- vpc.add_flow_log(
- "cms-vpc-flow-log",
- destination=aws_ec2.FlowLogDestination.to_cloud_watch_logs(
- log_group=vpc_log_group,
- iam_role=aws_iam.Role(
- self,
- "cms-vpc-cloudwatch-role",
- assumed_by=aws_iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"),
- inline_policies={
- "cms-vpc-cloudwatch-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "logs:CreateLogGroup",
- "logs:CreateLogStream",
- "logs:PutLogEvents",
- ],
- resources=[
- vpc_log_group.log_group_arn,
- vpc_log_group.log_group_arn + ":log-stream:*",
- ],
- ),
- ]
- )
- },
- ),
- ),
- )
-
- # this ensures the VPC is deleted first during teardown, eliminating the deletion race condition
- vpc.node.add_dependency(vpc_log_group)
-
- exclude_list = [
- ".github",
- ".pytest_cache",
- ".vscode",
- "node_modules",
- "examples",
- "dist-types",
- ".git",
- "cdk.out",
- ".mypy_cache",
- ".github",
- ".venv",
- "cms_dependency_layer",
- "provisioning_dependency_layer",
- "vs_dependency_layer",
- "alerts_dependency_layer",
- "ev_battery_dependency_layer",
- "user_authentication_dependency_layer",
- "api_dependency_layer",
- "None",
- ".chalice.out",
- "staging",
- "global-s3-assets",
- "regional-s3-assets",
- ]
-
- backstage_zip = aws_s3_assets.Asset(
- self,
- "cms-backstage-asset",
- path="./source/backstage",
- exclude=exclude_list,
- )
-
- backstage_ecr = aws_ecr.Repository(
- self,
- "backstage-ecr",
- image_scan_on_push=True,
- image_tag_mutability=aws_ecr.TagMutability.MUTABLE,
- repository_name="backstage",
- removal_policy=RemovalPolicy.DESTROY,
- )
- backstage_artifact = aws_codepipeline.Artifact(artifact_name="backstage")
-
- assume_cdk_role = aws_iam.Role(
- self,
- "backstage-deploy-role",
- assumed_by=aws_iam.ServicePrincipal("codebuild.amazonaws.com"),
- description="Backstage Configuration Deploy Role",
- role_name=f"{CmsConstants.STACK_NAME}-{Stack.of(self).region}-backstage-config-codebuild",
- inline_policies={
- "backstage-deploy-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- resources=[
- Stack.of(self).format_arn(
- region="",
- service="iam",
- resource="role",
- resource_name="cdk-*",
- )
- ],
- actions=["sts:AssumeRole"],
- ),
- ]
- )
- },
- )
-
- backstage_configuration_deploy_project = aws_codebuild.PipelineProject(
- self,
- "backstage-env-deploy-pipeline-project",
- project_name="backstage-configuration-deploy-project",
- check_secrets_in_plain_text_env_variables=True,
- encryption_key=aws_kms.Key(
- self, "backstage-env-deploy-key", enable_key_rotation=True
- ),
- build_spec=aws_codebuild.BuildSpec.from_source_filename(
- "./cdk/source/infrastructure/buildspecs/backstage_env_buildspec.json"
- ),
- environment=aws_codebuild.BuildEnvironment(
- compute_type=aws_codebuild.ComputeType.LARGE,
- build_image=aws_codebuild.LinuxBuildImage.STANDARD_7_0,
- ),
- environment_variables={
- "BACKSTAGE_VPC_ID": aws_codebuild.BuildEnvironmentVariable(
- value=vpc.vpc_id,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "AWS_REGION": aws_codebuild.BuildEnvironmentVariable(
- value=Stack.of(self).region,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "AWS_ACCOUNT_ID": aws_codebuild.BuildEnvironmentVariable(
- value=Stack.of(self).account,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "STAGE": aws_codebuild.BuildEnvironmentVariable(
- value=CmsConstants.STAGE,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- },
- role=assume_cdk_role,
- )
-
- aws_ssm.StringParameter(
- self,
- "ssm-admin-email",
- string_value=self.node.get_context("user-email"),
- description="The Cognito admin user",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/admin-email",
- )
- aws_ssm.StringParameter(
- self,
- "ssm-username",
- string_value=self.node.get_context("user-email").split("@")[0],
- description="The username to access the UI",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/username",
- )
- aws_ssm.StringParameter(
- self,
- "ssm-backstage-name",
- string_value=self.node.get_context("backstage-name"),
- description="The name to display on Backstage",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/backstage-name",
- )
- aws_ssm.StringParameter(
- self,
- "ssm-backstage-org",
- string_value=self.node.get_context("backstage-org"),
- description="The organization to display on Backstage",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/backstage-org",
- )
- aws_ssm.StringParameter(
- self,
- "ssm-backstage-log-level",
- string_value=self.node.get_context("backstage-log-level"),
- description="Level of logs to display (trace, debug, info, warn, error, critical)",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/backstage-log-level",
- )
- aws_ssm.StringParameter(
- self,
- "ssm-node-env",
- string_value="production",
- description="Node context (production or development)",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/node-env",
- )
- aws_ssm.StringParameter(
- self,
- "ssm-route53-zone-name",
- string_value=self.node.get_context("route53-zone-name"),
- description="The name of the hosted zone to deploy in",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/route53-zone-name",
- )
- aws_ssm.StringParameter(
- self,
- "ssm-route53-base-domain",
- string_value=self.node.get_context("route53-base-domain"),
- description="The name of the base domain to deploy in",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/route53-base-domain",
- )
- aws_ssm.StringParameter(
- self,
- "ssm-web-port",
- string_value=self.node.get_context("web-port"),
- description="The port used to reach Backstage (default: 443)",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/web-port",
- )
- aws_ssm.StringParameter(
- self,
- "ssm-web-scheme",
- string_value=self.node.get_context("web-scheme"),
- description="The scheme used to reach Backstage (default: https)",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/web-scheme",
- )
- aws_ssm.StringParameter(
- self,
- "ssm-backend-secret",
- string_value=backend_secret_object.secret_arn,
- description="Backend secret",
- parameter_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/secret-arns/backend-secret",
- )
-
- backstage_pipeline_project = aws_codebuild.PipelineProject(
- self,
- "backstage-build-pipeline-project",
- project_name="backstage-build-image",
- check_secrets_in_plain_text_env_variables=True,
- build_spec=aws_codebuild.BuildSpec.from_source_filename(
- "./cdk/source/infrastructure/buildspecs/backstage_image_buildspec.json"
- ),
- encryption_key=aws_kms.Key(
- self, "backstage-build-key", enable_key_rotation=True
- ),
- environment=aws_codebuild.BuildEnvironment(
- compute_type=aws_codebuild.ComputeType.LARGE,
- build_image=aws_codebuild.LinuxBuildImage.STANDARD_7_0,
- privileged=True,
- ),
- cache=aws_codebuild.Cache.local(
- aws_codebuild.LocalCacheMode.DOCKER_LAYER,
- aws_codebuild.LocalCacheMode.CUSTOM,
- ),
- environment_variables={
- "BACKSTAGE_NAME": aws_codebuild.BuildEnvironmentVariable(
- value=self.node.get_context("backstage-name"),
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "BACKSTAGE_ORG": aws_codebuild.BuildEnvironmentVariable(
- value=self.node.get_context("backstage-org"),
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "WEB_SCHEME": aws_codebuild.BuildEnvironmentVariable(
- value=self.node.get_context("web-scheme"),
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "WEB_HOSTNAME": aws_codebuild.BuildEnvironmentVariable(
- value=self.node.get_context("route53-zone-name"),
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "WEB_PORT": aws_codebuild.BuildEnvironmentVariable(
- value=self.node.get_context("web-port"),
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "DOCKER_BUILDKIT": aws_codebuild.BuildEnvironmentVariable(
- value=1,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "BACKSTAGE_VPC_ID": aws_codebuild.BuildEnvironmentVariable(
- value=vpc.vpc_id,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "IMAGE_NAME": aws_codebuild.BuildEnvironmentVariable(
- value="backstage",
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "IMAGE_TAG": aws_codebuild.BuildEnvironmentVariable(
- value="latest",
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "AWS_DEFAULT_REGION": aws_codebuild.BuildEnvironmentVariable(
- value=Stack.of(self).region,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "AWS_ACCOUNT_ID": aws_codebuild.BuildEnvironmentVariable(
- value=Stack.of(self).account,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "STAGE": aws_codebuild.BuildEnvironmentVariable(
- value=CmsConstants.STAGE,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "NODE_OPTIONS": aws_codebuild.BuildEnvironmentVariable(
- value="--max-old-space-size=8192",
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- },
- role=aws_iam.Role(
- self,
- "backstage-build-role",
- assumed_by=aws_iam.ServicePrincipal("codebuild.amazonaws.com"),
- description="Backstage Build Role",
- role_name=f"{CmsConstants.STACK_NAME}-{Stack.of(self).region}-backstage-build-codebuild",
- inline_policies={
- "backstage-build-secretsmanager-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "secretsmanager:GetSecretValue",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="secretsmanager",
- resource="secret",
- resource_name=f"/{CmsConstants.STAGE}/cms-backstage/*",
- arn_format=ArnFormat.COLON_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="secretsmanager",
- resource="secret",
- resource_name=f"{CmsConstants.STACK_NAME}/backend-secret",
- arn_format=ArnFormat.COLON_RESOURCE_NAME,
- ),
- ],
- ),
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=["ssm:GetParameter", "ssm:GetParameters"],
- resources=[
- Stack.of(self).format_arn(
- service="ssm",
- resource="parameter",
- resource_name=f"{CmsConstants.STAGE}/*",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- ],
- ),
- ]
- ),
- "backstage-build-ssm-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "ssm:GetParameter",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="ssm",
- resource="parameter",
- resource_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/*",
- arn_format=ArnFormat.COLON_RESOURCE_NAME,
- ),
- ],
- ),
- ]
- ),
- },
- ),
- )
- backstage_deploy_project = aws_codebuild.PipelineProject(
- self,
- "backstage-deploy-pipeline-project",
- project_name="backstage-deploy-project",
- check_secrets_in_plain_text_env_variables=True,
- encryption_key=aws_kms.Key(
- self, "backstage-deploy-key", enable_key_rotation=True
- ),
- build_spec=aws_codebuild.BuildSpec.from_source_filename(
- "./cdk/source/infrastructure/buildspecs/backstage_deploy_buildspec.json"
- ),
- environment=aws_codebuild.BuildEnvironment(
- compute_type=aws_codebuild.ComputeType.LARGE,
- build_image=aws_codebuild.LinuxBuildImage.STANDARD_7_0,
- ),
- environment_variables={
- "BACKSTAGE_VPC_ID": aws_codebuild.BuildEnvironmentVariable(
- value=vpc.vpc_id,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "AWS_REGION": aws_codebuild.BuildEnvironmentVariable(
- value=Stack.of(self).region,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "AWS_ACCOUNT_ID": aws_codebuild.BuildEnvironmentVariable(
- value=Stack.of(self).account,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "BACKEND_SECRET": aws_codebuild.BuildEnvironmentVariable(
- value=backend_secret_object.secret_arn,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "NODE_OPTIONS": aws_codebuild.BuildEnvironmentVariable(
- value="--max-old-space-size=8192",
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- "STAGE": aws_codebuild.BuildEnvironmentVariable(
- value=CmsConstants.STAGE,
- type=aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
- ),
- },
- role=assume_cdk_role,
- )
-
- backstage_ecr.grant_pull_push(backstage_pipeline_project)
- backstage_ecr.grant(backstage_pipeline_project, "ecr:*")
-
- backstage_pipeline = aws_codepipeline.Pipeline( # pylint: disable=W0612
- self,
- "backstage-code-pipeline",
- pipeline_name="Backstage-Pipeline",
- enable_key_rotation=True,
- restart_execution_on_update=True,
- stages=[
- aws_codepipeline.StageOptions(
- stage_name="Source-Stage-Backstage",
- actions=[
- aws_codepipeline_actions.S3SourceAction(
- action_name="S3-Source-Backstage-Asset",
- bucket_key=backstage_zip.s3_object_key,
- bucket=backstage_zip.bucket,
- output=backstage_artifact,
- trigger=aws_codepipeline_actions.S3Trigger.NONE,
- )
- ],
- ),
- aws_codepipeline.StageOptions(
- stage_name="Env-Deploy-Stage-Backstage",
- actions=[
- aws_codepipeline_actions.CodeBuildAction(
- input=backstage_artifact,
- extra_inputs=[],
- action_name="Env-Deploy",
- project=backstage_configuration_deploy_project,
- outputs=[],
- )
- ],
- ),
- aws_codepipeline.StageOptions(
- stage_name="Build-Stage-Backstage",
- actions=[
- aws_codepipeline_actions.CodeBuildAction(
- input=backstage_artifact,
- action_name="Build-Image",
- project=backstage_pipeline_project,
- outputs=[],
- )
- ],
- ),
- aws_codepipeline.StageOptions(
- stage_name="Deploy-Stage-Backstage",
- actions=[
- aws_codepipeline_actions.CodeBuildAction(
- input=backstage_artifact,
- extra_inputs=[backstage_artifact],
- action_name="Deploy",
- project=backstage_deploy_project,
- outputs=[],
- )
- ],
- ),
- ],
- role=aws_iam.Role(
- self,
- "backstage-pipeline-role",
- assumed_by=aws_iam.ServicePrincipal("codepipeline.amazonaws.com"),
- description="Backstage Pipeline Role",
- role_name=f"{CmsConstants.STACK_NAME}-{Stack.of(self).region}-backstage-codepipeline",
- inline_policies={
- "backstage-s3-asset": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "s3:GetBucketAcl",
- "s3:GetBucketLocation",
- "s3:GetBucketVersioning",
- "s3:GetObject",
- "s3:GetObjectAcl",
- "s3:GetObjectAttributes",
- "s3:GetObjectVersion",
- "s3:GetObjectVersionAcl",
- "s3:GetObjectVersionTagging",
- "s3:ListAllMyBuckets",
- "s3:ListBucket",
- "s3:ListBucketVersions",
- ],
- resources=[
- backstage_zip.bucket.bucket_arn,
- backstage_zip.bucket.arn_for_objects(
- backstage_zip.s3_object_key
- ),
- ],
- ),
- ]
- ),
- "backstage-pipeline-role": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "secretsmanager:GetSecretValue",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="secretsmanager",
- resource="secret",
- resource_name=f"/{CmsConstants.STAGE}/cms-backstage/*",
- arn_format=ArnFormat.COLON_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="secretsmanager",
- resource="secret",
- resource_name=f"{CmsConstants.STACK_NAME}/backend-secret",
- arn_format=ArnFormat.COLON_RESOURCE_NAME,
- ),
- ],
- ),
- ]
- ),
- "backstage-pipeline-ssm-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "ssm:GetParameter",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="ssm",
- resource="parameter",
- resource_name=f"/{CmsConstants.STAGE}/{CmsConstants.APP_NAME}/*",
- arn_format=ArnFormat.COLON_RESOURCE_NAME,
- ),
- ],
- ),
- ]
- ),
- },
- ),
- )
-
- add_slack_chatbot(backstage_pipeline)
-
- def add_slack_notification_codepipeline(
- self,
- is_arn_set: bool,
- slack_chatbot_arn: str,
- codepipeline: Union[aws_codepipeline.Pipeline, pipelines.CodePipeline],
- ) -> Optional[aws_chatbot.ISlackChannelConfiguration]:
- if not is_arn_set:
- return None
-
- pipeline: aws_codepipeline.Pipeline = getattr(
- codepipeline, "pipeline", codepipeline # type: ignore
- )
-
- chatbot_target = (
- aws_chatbot.SlackChannelConfiguration.from_slack_channel_configuration_arn(
- self,
- f"{pipeline.node.id}-chatbot-arn",
- slack_channel_configuration_arn=slack_chatbot_arn,
- )
- )
-
- pipeline.notify_on_any_stage_state_change(
- f"{pipeline.node.id}-notify",
- target=chatbot_target,
- notification_rule_name=f"{CmsConstants.STACK_NAME}-{Stack.of(self).region}-{pipeline.pipeline_name}-notify",
- )
-
- return chatbot_target
diff --git a/source/infrastructure/stacks/components/proton_environment.py b/source/infrastructure/stacks/components/proton_environment.py
deleted file mode 100644
index 5087656e..00000000
--- a/source/infrastructure/stacks/components/proton_environment.py
+++ /dev/null
@@ -1,347 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-# Standard Library
-import os
-import tarfile
-from typing import Any
-
-# Third Party Libraries
-from aws_cdk import (
- ArnFormat,
- CustomResource,
- Stack,
- aws_iam,
- aws_kms,
- aws_s3,
- aws_s3_deployment,
-)
-from constructs import Construct
-
-# Connected Mobility Solution on AWS
-from ...constructs.custom_resource_lambda import CustomResourceLambdaConstruct
-
-
-class ProtonEnvironment(Construct):
- def __init__( # too-many-locals: ignore
- self,
- scope: Stack,
- stack_id: str,
- custom_resource_construct: CustomResourceLambdaConstruct,
- **kwargs: Any,
- ) -> None: # too-many-locals: ignore
- super().__init__(scope, stack_id, **kwargs)
-
- s3_key = aws_kms.Key(
- self,
- "proton-environment-s3-key",
- enable_key_rotation=True,
- )
-
- proton_environment_s3_key_prefix = "cms_environment_templates"
-
- environment_bucket = aws_s3.Bucket(
- self,
- "proton-environment-bucket",
- block_public_access=aws_s3.BlockPublicAccess.BLOCK_ALL,
- enforce_ssl=True,
- server_access_logs_prefix="proton-environment-bucket/",
- encryption_key=s3_key,
- versioned=True,
- encryption=aws_s3.BucketEncryption.KMS,
- )
-
- code_build_iam_role = aws_iam.Role(
- self,
- "proton-code-build-role",
- assumed_by=aws_iam.ServicePrincipal("codebuild.amazonaws.com"),
- inline_policies={
- "s3-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "s3:GetObject",
- "s3:GetBucketLocation",
- "s3:ListBucket",
- ],
- resources=[
- environment_bucket.bucket_arn,
- f"arn:aws:s3:::cdk-*-assets-{Stack.of(self).account}-{Stack.of(self).region}",
- ],
- )
- ]
- ),
- "cloudwatch-logs-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "logs:CreateLogGroup",
- "logs:CreateLogStream",
- "logs:PutLogEvents",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="logs",
- resource="log-group",
- resource_name="/aws/codebuild/AWSProton-*:log-stream:*",
- arn_format=ArnFormat.COLON_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="logs",
- resource="log-group",
- resource_name="/aws/codebuild/AWSProton-*",
- arn_format=ArnFormat.COLON_RESOURCE_NAME,
- ),
- ],
- )
- ]
- ),
- "cloudformation-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "cloudformation:DescribeStacks",
- "cloudformation:CreateChangeSet",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="cloudformation",
- resource="stack",
- resource_name="cms-environment/*",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="cloudformation",
- resource="stack",
- resource_name="CDKToolkit/*",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- ],
- ),
- ]
- ),
- "ssm-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=["ssm:GetParameter"],
- resources=[
- Stack.of(self).format_arn(
- service="ssm",
- resource="parameter",
- resource_name="cdk-bootstrap/*/*",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- ],
- )
- ]
- ),
- "proton-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- actions=["proton:NotifyResourceDeploymentStatusChange"],
- effect=aws_iam.Effect.ALLOW,
- resources=[
- Stack.of(self).format_arn(
- service="proton",
- resource="environment",
- resource_name="cms_environment",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="proton",
- resource="service",
- resource_name="*",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- ],
- )
- ]
- ),
- "iam-policy": aws_iam.PolicyDocument(
- statements=[
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=["iam:PassRole"],
- resources=[
- f"arn:aws:iam::{Stack.of(self).account}:role/cdk-*-cfn-exec-role-{Stack.of(self).account}-{Stack.of(self).region}"
- ],
- )
- ]
- ),
- },
- )
-
- code_build_iam_role.add_to_principal_policy(
- aws_iam.PolicyStatement(
- actions=["sts:AssumeRole"],
- effect=aws_iam.Effect.ALLOW,
- resources=[
- code_build_iam_role.role_arn,
- f"arn:aws:iam::{Stack.of(self).account}:role/cdk-*-file-publishing-role-{Stack.of(self).account}-{Stack.of(self).region}",
- f"arn:aws:iam::{Stack.of(self).account}:role/cdk-*-deploy-role-{Stack.of(self).account}-{Stack.of(self).region}",
- ],
- )
- )
-
- environment_folder_path = os.path.abspath(
- os.path.join("templates", "environments")
- )
-
- def filter_environment_tar_folders(tarinfo: Any) -> Any:
- if ".venv" in tarinfo.name.split(os.path.sep):
- return None
- return tarinfo
-
- tar_folder_path = os.path.abspath(os.path.join("cdk.out", "environment_tars"))
- environments = os.listdir(environment_folder_path)
- if not os.path.exists(tar_folder_path):
- os.makedirs(tar_folder_path)
- for environment in environments:
- environment_path = os.path.join(environment_folder_path, environment)
- tar_file_path = os.path.join(tar_folder_path, environment) + ".tar.gz"
- if os.path.isdir(environment_path):
- with tarfile.open(tar_file_path, "w:gz") as tar: # NOSONAR
- tar.add(
- environment_path,
- arcname=os.path.basename(environment_path),
- filter=filter_environment_tar_folders,
- )
-
- s3_environment_templates = aws_s3_deployment.BucketDeployment(
- self,
- "proton-environment-templates-custom-deployment",
- sources=[aws_s3_deployment.Source.asset(tar_folder_path)],
- destination_bucket=environment_bucket,
- destination_key_prefix=proton_environment_s3_key_prefix,
- prune=True,
- )
-
- custom_resource_construct.add_policy_to_custom_resource_lambda(
- policy=aws_iam.Policy(
- self,
- "custom-resource-policy",
- document=aws_iam.PolicyDocument(
- statements=[
- # S3
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "s3:GetObject",
- "s3:GetBucketLocation",
- "s3:ListBucket",
- ],
- resources=[
- environment_bucket.bucket_arn,
- f"{environment_bucket.bucket_arn}/{proton_environment_s3_key_prefix}/*",
- ],
- ),
- # Proton
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "proton:GetEnvironment",
- "proton:UpdateEnvironment",
- "proton:CreateEnvironment",
- "proton:CreateEnvironmentTemplate",
- "proton:CreateEnvironmentTemplateVersion",
- "proton:GetEnvironmentTemplateVersion",
- "proton:GetEnvironmentTemplateMinorVersion",
- "proton:GetEnvironmentTemplateMajorVersion",
- "proton:UpdateEnvironmentTemplateVersion",
- "proton:UpdateEnvironmentTemplateMinorVersion",
- "proton:UpdateEnvironmentTemplateMajorVersion",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="proton",
- resource="environment",
- resource_name="cms_environment",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- Stack.of(self).format_arn(
- service="proton",
- resource="environment-template",
- resource_name="cms_environment",
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- ),
- ],
- ),
- # IAM
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=["iam:PassRole"],
- resources=[code_build_iam_role.role_arn],
- ),
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=["iam:CreateServiceLinkedRole"],
- resources=[
- Stack.of(self).format_arn(
- service="iam",
- resource="role",
- resource_name="aws-service-role/codebuild.proton.amazonaws.com/AWSServiceRoleForProtonCodeBuildProvisioning",
- region="", # This is necessary since the SLR does not specify region in its ARN
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- )
- ],
- conditions={
- "StringLike": {
- "iam:AWSServiceName": "codebuild.proton.amazonaws.com"
- }
- },
- ),
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "iam:AttachRolePolicy",
- "iam:PutRolePolicy",
- "iam:UpdateRoleDescription",
- "iam:DeleteServiceLinkedRole",
- "iam:GetServiceLinkedRoleDeletionStatus",
- ],
- resources=[
- Stack.of(self).format_arn(
- service="iam",
- resource="role",
- resource_name="aws-service-role/codebuild.proton.amazonaws.com/AWSServiceRoleForProtonCodeBuildProvisioning",
- region="", # This is necessary since the SLR does not specify region in its ARN
- arn_format=ArnFormat.SLASH_RESOURCE_NAME,
- )
- ],
- ),
- # KMS
- aws_iam.PolicyStatement(
- effect=aws_iam.Effect.ALLOW,
- actions=[
- "kms:Decrypt",
- "kms:GenerateDataKey",
- ],
- resources=[
- environment_bucket.encryption_key.key_arn, # type: ignore [union-attr]
- ],
- ),
- ]
- ),
- )
- )
-
- create_proton_environments = CustomResource(
- self,
- "create-proton-templates",
- service_token=custom_resource_construct.custom_resource_lambda.function_arn,
- resource_type="Custom::CreateProtonEnvironment",
- properties={
- "Resource": "CreateProtonEnvironment",
- "TEMPLATE_S3_BUCKET_NAME": s3_environment_templates.deployed_bucket.bucket_name,
- "TEMPLATE_S3_KEY_PREFIX": proton_environment_s3_key_prefix,
- "CODE_BUILD_IAM_ROLE": code_build_iam_role.role_arn,
- },
- )
-
- create_proton_environments.node.add_dependency(s3_environment_templates)
- create_proton_environments.node.add_dependency(code_build_iam_role)
diff --git a/source/lib/.pre-commit-config.yaml b/source/lib/.pre-commit-config.yaml
new file mode 100644
index 00000000..f3f3ee85
--- /dev/null
+++ b/source/lib/.pre-commit-config.yaml
@@ -0,0 +1,121 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.5.0
+ hooks:
+ - id: name-tests-test
+ name: (Common) Check test naming
+ args: ["--pytest-test-first"]
+ exclude: (tests/?.*/fixture(s)?_.*\.py$)
+ - id: check-executables-have-shebangs
+ name: (Common) Check executables have shebangs
+ - id: fix-byte-order-marker
+ name: (Common) Fix byte order marker
+ - id: check-case-conflict
+ name: (Common) Check case conflict
+ - id: check-json
+ name: (Common) Check json
+ - id: check-yaml
+ name: (Common) Check yaml
+ args: [--allow-multiple-documents, --unsafe]
+ - id: check-toml
+ name: (Common) Check toml
+ - id: check-merge-conflict
+ name: (Common) Check for merge conflicts
+ - id: check-added-large-files
+ name: (Common) Check for added large files
+ exclude: |
+ (?x)^(
+ ^.*/package-lock.json |
+ ^.*/yarn.lock |
+ ^.*/Pipfile.lock
+ )$
+ - id: end-of-file-fixer
+ name: (Common) Fix end of files
+ - id: fix-encoding-pragma
+ name: (Common) Fix python encoding pragma
+ - id: trailing-whitespace
+ name: (Common) Trim trailing whitespace
+ - id: mixed-line-ending
+ name: (Common) Mixed line ending
+ - id: detect-aws-credentials
+ name: (Common) Detect AWS credentials
+ args: ["--credentials-file", "~/.ada/credentials"]
+ - id: detect-private-key
+ name: (Common) Detect private keys
+ - repo: https://github.com/Lucas-C/pre-commit-hooks
+ rev: v1.5.1
+ hooks:
+ - id: insert-license
+ name: (Common) Insert license header (python)
+ files: \.py$
+ args:
+ - --license-filepath
+ - ./source/lib/license_header.txt
+ - --detect-license-in-X-top-lines=3
+ - id: insert-license
+ name: (Common) Insert license header (typescript and javascript)
+ files: \.tsx$|\.ts$|\.js$|\.jsx$
+ args:
+ - --license-filepath
+ - ./source/lib/license_header.txt
+ - --comment-style
+ - // # defaults to Python's # syntax, requires changing for typescript syntax.
+ - --detect-license-in-X-top-lines=3
+ - repo: https://github.com/psf/black
+ rev: 22.3.0
+ hooks:
+ - id: black
+ name: (Common) Black
+ - repo: https://github.com/hadialqattan/pycln
+ rev: v2.1.3
+ hooks:
+ - id: pycln
+ name: (Common) Pycln
+ - repo: https://github.com/pycqa/isort
+ rev: 5.12.0
+ hooks:
+ - id: isort
+ name: (Common) Isort (python)
+ args: ["--skip-glob", "**/node_modules/* **/.venv/*", "--settings-path", "./source/lib/pyproject.toml"]
+ - repo: https://github.com/PyCQA/bandit
+ rev: 1.7.4
+ hooks:
+ - id: bandit
+ name: (Common) Bandit
+ args: ["-c", "./source/lib/pyproject.toml"]
+ additional_dependencies: [ "bandit[toml]" ]
+ - repo: https://github.com/pypa/pip-audit
+ rev: v2.6.1
+ hooks:
+ - id: pip-audit
+ name: (Common) Pip audit
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v3.1.0
+ hooks:
+ - id: prettier
+ name: (Common) Prettier
+ types_or: [javascript, jsx, ts, tsx]
+ # Local
+ - repo: local
+ hooks:
+ - id: shellcheck
+ name: (Common) Shellchecker
+ entry: shellcheck
+ args: ["-x"]
+ types: [shell]
+ language: system
+ - id: pylint
+ name: (Common) pylint
+ entry: pylint
+ args: ["--extension-pkg-allow-list", "math", "--rcfile", "./source/lib/pyproject.toml"]
+ types: [python]
+ language: system
+ - id: mypy
+ name: (Common) mypy
+ entry: mypy
+ types_or: [python, pyi]
+ args: ["--strict", "--cache-dir", "./source/lib/.mypy_cache", "--config-file", "./source/lib/pyproject.toml"]
+ language: system
diff --git a/source/lib/.python-version b/source/lib/.python-version
new file mode 100644
index 00000000..c8cfe395
--- /dev/null
+++ b/source/lib/.python-version
@@ -0,0 +1 @@
+3.10
diff --git a/source/lib/Makefile b/source/lib/Makefile
new file mode 100644
index 00000000..9e23a379
--- /dev/null
+++ b/source/lib/Makefile
@@ -0,0 +1,43 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+.DEFAULT_GOAL := help
+
+# ========================================================
+# LIB METADATA
+# ========================================================
+export MODULE_NAME ?= $(shell python3 ./setup.py --name)
+export MODULE_VERSION ?= $(shell python3 ./setup.py --version)
+export MODULE_DESCRIPTION ?= $(shell python3 ./setup.py --description)
+export MODULE_AUTHOR ?= $(shell python3 ./setup.py --author)
+
+SOLUTION_PATH := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..)
+MODULE_PATH := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
+
+include ${SOLUTION_PATH}/makefiles/common_config.mk
+include ${SOLUTION_PATH}/makefiles/global_targets.mk
+include ${SOLUTION_PATH}/makefiles/module_targets.mk
+
+## ========================================================
+## INSTALL
+## ========================================================
+.PHONY: install
+install: pipenv-install ## Installs the resources and dependencies required to build the solution.
+ @printf "%bInstall finished.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: deploy
+deploy: ## Deploy library to pypi - NOT IMPLEMENTED
+ @printf "%bLibrary deployed to pypi. version=%s%b\n" "${GREEN}" "${MODULE_VERSION}" "${NC}"
+
+.PHONY: destroy
+destroy: ## NOT IMPLEMENTED
+ @printf "%bLibrary does not support destruction.%b" "${MAGENTA}" "${NC}"
+
+.PHONY: pipenv-setup-sync
+pipenv-setup-sync: ## Using pipenv-setup, sync Pipfile.lock and setup.py
+ifeq (, $(shell which pipenv-setup))
+ $(error pipenv-setup is required to sync setup.py. Run `make install` prior to this target.)
+endif
+ pipenv-setup sync --pipfile
+ @printf "%bRunning `black` to format setup.py.%b\n" "${MAGENTA}" "${NC}"
+ -pre-commit run black --files ./setup.py
diff --git a/source/lib/Pipfile b/source/lib/Pipfile
new file mode 100644
index 00000000..30713c5c
--- /dev/null
+++ b/source/lib/Pipfile
@@ -0,0 +1,38 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+aws_lambda_powertools = {extras=["tracer", "validation"], version=">=2.4.0"}
+cattrs = ">=22.1.0"
+
+[dev-packages]
+aws-cdk-lib = ">=2.63.2"
+boto3 = ">=1.26.0"
+boto3-stubs = {extras = ["essential", "secretsmanager", "ssm"], version = "*"}
+cdk-nag = "*"
+moto = "*"
+mypy = "*"
+pipenv-setup = "==3.2.0" # unmaintained, only used in cms_common Makefile target for manually syncing setup.py and Pipfile.lock
+pre-commit = "*"
+pycln = "*"
+pylint = "*"
+pytest = "*"
+pytest-cov = "*"
+pytest-mock = "*"
+requests = "*"
+syrupy = "*"
+toml = "*"
+types-boto3 = "*"
+types-pyyaml = "*"
+types-requests = ">=2.28.1"
+types-setuptools = "*"
+types-urllib3 = "*"
+types-toml = "*"
+vistir = "==0.6.1" # Necessary for resolving a `vistir` version conflict with `pipenv-setup`
+wheel = "*"
+wrapt = "*"
+
+[requires]
+python_version = "3.10"
diff --git a/source/lib/Pipfile.lock b/source/lib/Pipfile.lock
new file mode 100644
index 00000000..21d802cf
--- /dev/null
+++ b/source/lib/Pipfile.lock
@@ -0,0 +1,1475 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "10448889c3f130af28caa8adbfb766bc85661defe44eae9d70ac316897658227"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.10"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "attrs": {
+ "hashes": [
+ "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30",
+ "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==23.2.0"
+ },
+ "aws-lambda-powertools": {
+ "extras": [
+ "tracer",
+ "validation"
+ ],
+ "hashes": [
+ "sha256:02fafbdaaa0a89faaf8c49777a22996aaba465a099d69b4f1fbfd8c3ae47fc41",
+ "sha256:cfcc41d7125b9527b8fd8c4e3e4b30b971f915c32ec0c6e39573fd9f298a63a8"
+ ],
+ "markers": "python_version >= '3.8' and python_full_version < '4.0.0'",
+ "version": "==2.34.2"
+ },
+ "aws-xray-sdk": {
+ "hashes": [
+ "sha256:0bbfdbc773cfef4061062ac940b85e408297a2242f120bcdfee2593209b1e432",
+ "sha256:f6803832dc08d18cc265e2327a69bfa9ee41c121fac195edc9745d04b7a566c3"
+ ],
+ "version": "==2.12.1"
+ },
+ "botocore": {
+ "hashes": [
+ "sha256:01d5156247f991b3466a8404e3d7460a9ecbd9b214f9992d6ba797d9ddc6f120",
+ "sha256:5086217442e67dd9de36ec7e87a0c663f76b7790d5fb6a12de565af95e87e319"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "cattrs": {
+ "hashes": [
+ "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108",
+ "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==23.2.3"
+ },
+ "exceptiongroup": {
+ "hashes": [
+ "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14",
+ "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==1.2.0"
+ },
+ "fastjsonschema": {
+ "hashes": [
+ "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0",
+ "sha256:e3126a94bdc4623d3de4485f8d468a12f02a67921315ddc87836d6e456dc789d"
+ ],
+ "version": "==2.19.1"
+ },
+ "jmespath": {
+ "hashes": [
+ "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
+ "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.0.1"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==2.8.2"
+ },
+ "six": {
+ "hashes": [
+ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+ "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==1.16.0"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
+ "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.10.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
+ "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
+ ],
+ "markers": "python_version >= '3.10'",
+ "version": "==2.0.7"
+ },
+ "wrapt": {
+ "hashes": [
+ "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc",
+ "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81",
+ "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09",
+ "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e",
+ "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca",
+ "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0",
+ "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb",
+ "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487",
+ "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40",
+ "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c",
+ "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060",
+ "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202",
+ "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41",
+ "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9",
+ "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b",
+ "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664",
+ "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d",
+ "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362",
+ "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00",
+ "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc",
+ "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1",
+ "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267",
+ "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956",
+ "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966",
+ "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1",
+ "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228",
+ "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72",
+ "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d",
+ "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292",
+ "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0",
+ "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0",
+ "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36",
+ "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c",
+ "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5",
+ "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f",
+ "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73",
+ "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b",
+ "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2",
+ "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593",
+ "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39",
+ "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389",
+ "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf",
+ "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf",
+ "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89",
+ "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c",
+ "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c",
+ "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f",
+ "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440",
+ "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465",
+ "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136",
+ "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b",
+ "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8",
+ "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3",
+ "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8",
+ "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6",
+ "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e",
+ "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f",
+ "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c",
+ "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e",
+ "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8",
+ "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2",
+ "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020",
+ "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35",
+ "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d",
+ "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3",
+ "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537",
+ "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809",
+ "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d",
+ "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a",
+ "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==1.16.0"
+ }
+ },
+ "develop": {
+ "astroid": {
+ "hashes": [
+ "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819",
+ "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"
+ ],
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==3.1.0"
+ },
+ "attrs": {
+ "hashes": [
+ "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30",
+ "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==23.2.0"
+ },
+ "aws-cdk-lib": {
+ "hashes": [
+ "sha256:03a98770dd58caa002ded8d2dcdd3f6f7451a95f86c8dba3b5f2b70e659429b3",
+ "sha256:b9ed68a5fd7f5b9056da58bd122c9c3faa6af1e92f4b6aff181a2ee57625aad1"
+ ],
+ "index": "pypi",
+ "markers": "python_version ~= '3.8'",
+ "version": "==2.130.0"
+ },
+ "aws-cdk.asset-awscli-v1": {
+ "hashes": [
+ "sha256:3ef87d6530736b3a7b0f777fe3b4297994dd40c3ce9306d95f80f48fb18036e8",
+ "sha256:96205ea2e5e132ec52fabfff37ea25b9b859498f167d05b32564c949822cd331"
+ ],
+ "markers": "python_version ~= '3.8'",
+ "version": "==2.2.202"
+ },
+ "aws-cdk.asset-kubectl-v20": {
+ "hashes": [
+ "sha256:346283e43018a43e3b3ca571de3f44e85d49c038dc20851894cb8f9b2052b164",
+ "sha256:7f0617ab6cb942b066bd7174bf3e1f377e57878c3e1cddc21d6b2d13c92d0cc1"
+ ],
+ "markers": "python_version ~= '3.7'",
+ "version": "==2.1.2"
+ },
+ "aws-cdk.asset-node-proxy-agent-v6": {
+ "hashes": [
+ "sha256:42cdbc1de2ed3f845e3eb883a72f58fc7e5554c2e0b6fcdb366c159778dce74d",
+ "sha256:e442673d4f93137ab165b75386761b1d46eea25fc5015e5145ae3afa9da06b6e"
+ ],
+ "markers": "python_version ~= '3.7'",
+ "version": "==2.0.1"
+ },
+ "boto3": {
+ "hashes": [
+ "sha256:2cd9463e738a184cbce8a6824027c22163c5f73e277a35ff5aa0fb0e845b4301",
+ "sha256:67732634dc7d0afda879bd9a5e2d0818a2c14a98bef766b95a3e253ea5104cb9"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "boto3-stubs": {
+ "extras": [
+ "essential",
+ "secretsmanager",
+ "ssm"
+ ],
+ "hashes": [
+ "sha256:3c3283d3982099cfbe6fee474f8eae42217b7cdfd98d5dd857ea952e29bdabf1",
+ "sha256:c04ece156a376745af34aefe7283e93f7066d8f2be2500297b129e3d46e0ac26"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "botocore": {
+ "hashes": [
+ "sha256:01d5156247f991b3466a8404e3d7460a9ecbd9b214f9992d6ba797d9ddc6f120",
+ "sha256:5086217442e67dd9de36ec7e87a0c663f76b7790d5fb6a12de565af95e87e319"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "botocore-stubs": {
+ "hashes": [
+ "sha256:8748b9fe01f66bb1e7b13f45e3336e2e2c5460d232816d45941573425459c66e",
+ "sha256:d0f4d9859d9f6affbe4b0b46e37fe729860eaab55ebefe7e09cf567396b2feda"
+ ],
+ "markers": "python_version >= '3.8' and python_version < '4.0'",
+ "version": "==1.34.51"
+ },
+ "cached-property": {
+ "hashes": [
+ "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130",
+ "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"
+ ],
+ "version": "==1.5.2"
+ },
+ "cattrs": {
+ "hashes": [
+ "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108",
+ "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==23.2.3"
+ },
+ "cdk-nag": {
+ "hashes": [
+ "sha256:602d8a91252424f557f2dc991dca413dbdd7ae656303d961a849634a4181532a",
+ "sha256:8f62603886eac9072aa77fc79700efdc6d1ac44a7b8537516f8adf849d59dae9"
+ ],
+ "index": "pypi",
+ "markers": "python_version ~= '3.8'",
+ "version": "==2.28.48"
+ },
+ "cerberus": {
+ "hashes": [
+ "sha256:7649a5815024d18eb7c6aa5e7a95355c649a53aacfc9b050e9d0bf6bfa2af372",
+ "sha256:81011e10266ef71b6ec6d50e60171258a5b134d69f8fb387d16e4936d0d47642"
+ ],
+ "version": "==1.3.5"
+ },
+ "certifi": {
+ "hashes": [
+ "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
+ "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==2024.2.2"
+ },
+ "cffi": {
+ "hashes": [
+ "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc",
+ "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a",
+ "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417",
+ "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab",
+ "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520",
+ "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36",
+ "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743",
+ "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8",
+ "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed",
+ "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684",
+ "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56",
+ "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324",
+ "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d",
+ "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235",
+ "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e",
+ "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088",
+ "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000",
+ "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7",
+ "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e",
+ "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673",
+ "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c",
+ "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe",
+ "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2",
+ "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098",
+ "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8",
+ "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a",
+ "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0",
+ "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b",
+ "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896",
+ "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e",
+ "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9",
+ "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2",
+ "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b",
+ "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6",
+ "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404",
+ "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f",
+ "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0",
+ "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4",
+ "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc",
+ "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936",
+ "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba",
+ "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872",
+ "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb",
+ "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614",
+ "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1",
+ "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d",
+ "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969",
+ "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b",
+ "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4",
+ "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627",
+ "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956",
+ "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"
+ ],
+ "markers": "platform_python_implementation != 'PyPy'",
+ "version": "==1.16.0"
+ },
+ "cfgv": {
+ "hashes": [
+ "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9",
+ "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.4.0"
+ },
+ "chardet": {
+ "hashes": [
+ "sha256:0368df2bfd78b5fc20572bb4e9bb7fb53e2c094f60ae9993339e8671d0afb8aa",
+ "sha256:d3e64f022d254183001eccc5db4040520c0f23b1a3f33d6413e099eb7f126557"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==5.0.0"
+ },
+ "charset-normalizer": {
+ "hashes": [
+ "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027",
+ "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087",
+ "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786",
+ "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8",
+ "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09",
+ "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185",
+ "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574",
+ "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e",
+ "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519",
+ "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898",
+ "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269",
+ "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3",
+ "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f",
+ "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6",
+ "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8",
+ "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a",
+ "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73",
+ "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc",
+ "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714",
+ "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2",
+ "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc",
+ "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce",
+ "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d",
+ "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e",
+ "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6",
+ "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269",
+ "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96",
+ "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d",
+ "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a",
+ "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4",
+ "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77",
+ "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d",
+ "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0",
+ "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed",
+ "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068",
+ "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac",
+ "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25",
+ "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8",
+ "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab",
+ "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26",
+ "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2",
+ "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db",
+ "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f",
+ "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5",
+ "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99",
+ "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c",
+ "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d",
+ "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811",
+ "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa",
+ "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a",
+ "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03",
+ "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b",
+ "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04",
+ "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c",
+ "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001",
+ "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458",
+ "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389",
+ "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99",
+ "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985",
+ "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537",
+ "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238",
+ "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f",
+ "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d",
+ "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796",
+ "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a",
+ "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143",
+ "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8",
+ "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c",
+ "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5",
+ "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5",
+ "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711",
+ "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4",
+ "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6",
+ "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c",
+ "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7",
+ "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4",
+ "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b",
+ "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae",
+ "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12",
+ "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c",
+ "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae",
+ "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8",
+ "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887",
+ "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b",
+ "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4",
+ "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f",
+ "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5",
+ "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33",
+ "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519",
+ "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==3.3.2"
+ },
+ "click": {
+ "hashes": [
+ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+ "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==8.1.7"
+ },
+ "colorama": {
+ "hashes": [
+ "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
+ "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
+ "version": "==0.4.6"
+ },
+ "constructs": {
+ "hashes": [
+ "sha256:2972f514837565ff5b09171cfba50c0159dfa75ee86a42921ea8c86f2941b3d2",
+ "sha256:518551135ec236f9cc6b86500f4fbbe83b803ccdc6c2cb7684e0b7c4d234e7b1"
+ ],
+ "markers": "python_version ~= '3.7'",
+ "version": "==10.3.0"
+ },
+ "coverage": {
+ "extras": [
+ "toml"
+ ],
+ "hashes": [
+ "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa",
+ "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003",
+ "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f",
+ "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c",
+ "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e",
+ "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0",
+ "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9",
+ "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52",
+ "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e",
+ "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454",
+ "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0",
+ "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079",
+ "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352",
+ "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f",
+ "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30",
+ "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe",
+ "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113",
+ "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765",
+ "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc",
+ "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e",
+ "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501",
+ "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7",
+ "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2",
+ "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f",
+ "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4",
+ "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524",
+ "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c",
+ "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51",
+ "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840",
+ "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6",
+ "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee",
+ "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e",
+ "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45",
+ "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba",
+ "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d",
+ "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3",
+ "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10",
+ "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e",
+ "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb",
+ "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9",
+ "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a",
+ "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47",
+ "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1",
+ "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3",
+ "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914",
+ "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328",
+ "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6",
+ "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d",
+ "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0",
+ "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94",
+ "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc",
+ "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==7.4.3"
+ },
+ "cryptography": {
+ "hashes": [
+ "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee",
+ "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576",
+ "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d",
+ "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30",
+ "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413",
+ "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb",
+ "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da",
+ "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4",
+ "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd",
+ "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc",
+ "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8",
+ "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1",
+ "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc",
+ "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e",
+ "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8",
+ "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940",
+ "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400",
+ "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7",
+ "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16",
+ "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278",
+ "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74",
+ "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec",
+ "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1",
+ "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2",
+ "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c",
+ "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922",
+ "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a",
+ "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6",
+ "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1",
+ "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e",
+ "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac",
+ "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==42.0.5"
+ },
+ "dill": {
+ "hashes": [
+ "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca",
+ "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==0.3.8"
+ },
+ "distlib": {
+ "hashes": [
+ "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784",
+ "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"
+ ],
+ "version": "==0.3.8"
+ },
+ "exceptiongroup": {
+ "hashes": [
+ "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14",
+ "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==1.2.0"
+ },
+ "filelock": {
+ "hashes": [
+ "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e",
+ "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.13.1"
+ },
+ "identify": {
+ "hashes": [
+ "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791",
+ "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==2.5.35"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
+ "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==3.6"
+ },
+ "importlib-resources": {
+ "hashes": [
+ "sha256:308abf8474e2dba5f867d279237cd4076482c3de7104a40b41426370e891549b",
+ "sha256:9a0a862501dc38b68adebc82970140c9e4209fc99601782925178f8386339938"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==6.1.2"
+ },
+ "iniconfig": {
+ "hashes": [
+ "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
+ "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.0"
+ },
+ "isort": {
+ "hashes": [
+ "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109",
+ "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"
+ ],
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==5.13.2"
+ },
+ "jinja2": {
+ "hashes": [
+ "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
+ "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==3.1.3"
+ },
+ "jmespath": {
+ "hashes": [
+ "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
+ "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.0.1"
+ },
+ "jsii": {
+ "hashes": [
+ "sha256:1105bae271ae47c27cf31c1565c5157306efed5ad9323c9a27336f962f465716",
+ "sha256:175abc356603d98f18ab6f6aa74bfeae253e4e56340aef9dc40bbb1a6a59868b"
+ ],
+ "markers": "python_version ~= '3.8'",
+ "version": "==1.94.0"
+ },
+ "libcst": {
+ "hashes": [
+ "sha256:0cb92398236566f0b73a0c73f8a41a9c4906c793e8f7c2745f30e3fb141a34b5",
+ "sha256:13ca9fe82326d82feb2c7b0f5a320ce7ed0d707c32919dd36e1f40792459bf6f",
+ "sha256:1b5fecb2b26fa3c1efe6e05ef1420522bd31bb4dae239e4c41fdf3ddbd853aeb",
+ "sha256:1d45718f7e7a1405a16fd8e7fc75c365120001b6928bfa3c4112f7e533990b9a",
+ "sha256:2bbb4e442224da46b59a248d7d632ed335eae023a921dea1f5c72d2a059f6be9",
+ "sha256:38fbd56f885e1f77383a6d1d798a917ffbc6d28dc6b1271eddbf8511c194213e",
+ "sha256:3c7c0edfe3b878d64877671261c7b3ffe9d23181774bfad5d8fcbdbbbde9f064",
+ "sha256:4973a9d509cf1a59e07fac55a98f70bc4fd35e09781dffb3ec93ee32fc0de7af",
+ "sha256:5c0d548d92c6704bb07ce35d78c0e054cdff365def0645c1b57c856c8e112bb4",
+ "sha256:5e54389abdea995b39ee96ad736ed1b0b8402ed30a7956b7a279c10baf0c0294",
+ "sha256:6dd388c74c04434b41e3b25fc4a0fafa3e6abf91f97181df55e8f8327fd903cc",
+ "sha256:71dd69fff76e7edaf8fae0f63ffcdbf5016e8cd83165b1d0688d6856aa48186a",
+ "sha256:7f4919978c2b395079b64d8a654357854767adbabab13998b39c1f0bc67da8a7",
+ "sha256:82373a35711a8bb2a664dba2b7aeb20bbcce92a4db40af964e9cb2b976f989e7",
+ "sha256:8b56130f18aca9a98b3bcaf5962b2b26c2dcdd6d5132decf3f0b0b635f4403ba",
+ "sha256:968b93400e66e6711a29793291365e312d206dbafd3fc80219cfa717f0f01ad5",
+ "sha256:b4066dcadf92b183706f81ae0b4342e7624fc1d9c5ca2bf2b44066cb74bf863f",
+ "sha256:ba24b8cf789db6b87c6e23a6c6365f5f73cb7306d929397581d5680149e9990c",
+ "sha256:c0149d24a455536ff2e41b3a48b16d3ebb245e28035013c91bd868def16592a0",
+ "sha256:c80f36f4a02d530e28eac7073aabdea7c6795fc820773a02224021d79d164e8b",
+ "sha256:dded0e4f2e18150c4b07fedd7ef84a9abc7f9bd2d47cc1c485248ee1ec58e5cc",
+ "sha256:dece0362540abfc39cd2cf5c98cde238b35fd74a1b0167e2563e4b8bb5f47489",
+ "sha256:e01879aa8cd478bb8b1e4285cfd0607e64047116f7ab52bc2a787cde584cd686",
+ "sha256:f080e9af843ff609f8f35fc7275c8bf08b02c31115e7cd5b77ca3b6a56c75096",
+ "sha256:f2342634f6c61fc9076dc0baf21e9cf5ef0195a06e1e95c0c9dc583ba3a30d00"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==1.2.0"
+ },
+ "markupsafe": {
+ "hashes": [
+ "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
+ "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
+ "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
+ "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
+ "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
+ "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
+ "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
+ "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
+ "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
+ "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
+ "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
+ "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
+ "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
+ "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
+ "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
+ "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
+ "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
+ "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
+ "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
+ "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
+ "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
+ "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
+ "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
+ "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
+ "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
+ "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
+ "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
+ "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
+ "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
+ "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
+ "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
+ "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
+ "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
+ "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
+ "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
+ "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
+ "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
+ "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
+ "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
+ "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
+ "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
+ "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
+ "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
+ "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
+ "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
+ "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
+ "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
+ "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
+ "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
+ "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
+ "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
+ "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
+ "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
+ "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
+ "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
+ "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
+ "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
+ "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
+ "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
+ "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.1.5"
+ },
+ "mccabe": {
+ "hashes": [
+ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
+ "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.7.0"
+ },
+ "moto": {
+ "hashes": [
+ "sha256:71bb832a18b64f10fc4cec117b9b0e2305e5831d9a17eb74f6b9819ed7613843",
+ "sha256:7e27395e5c63ff9554ae14b5baa41bfe6d6b1be0e59eb02977c6ce28411246de"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==5.0.2"
+ },
+ "mypy": {
+ "hashes": [
+ "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6",
+ "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d",
+ "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02",
+ "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d",
+ "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3",
+ "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3",
+ "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3",
+ "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66",
+ "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259",
+ "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835",
+ "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd",
+ "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d",
+ "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8",
+ "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07",
+ "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b",
+ "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e",
+ "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6",
+ "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae",
+ "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9",
+ "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d",
+ "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a",
+ "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592",
+ "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218",
+ "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817",
+ "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4",
+ "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410",
+ "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==1.8.0"
+ },
+ "mypy-boto3-cloudformation": {
+ "hashes": [
+ "sha256:49d04c090dae3fd8289738ae592cac9d6faa5169684de40c2730b425bba2a32d",
+ "sha256:bfe5ec405eae6dae31dc9874729eef5e668e634eae8972032f00400d17bd2c7d"
+ ],
+ "version": "==1.34.32"
+ },
+ "mypy-boto3-dynamodb": {
+ "hashes": [
+ "sha256:126da0a29ca48502cfa9a26e3024341233d8419f7e03273cea17af7d38e724bd",
+ "sha256:1af7c80a0891edac29e5b70441122f6803eb772a3b7b498396eec30368232541"
+ ],
+ "version": "==1.34.46"
+ },
+ "mypy-boto3-ec2": {
+ "hashes": [
+ "sha256:702378c68af01c47c1fd6e739f16599b0c388045127a993e0cc41dbbff31cc0d",
+ "sha256:ea74f5a45f1c4bfa8c21604ab391d3c504b218c2db091488d7c803bd9b443c9c"
+ ],
+ "version": "==1.34.50"
+ },
+ "mypy-boto3-lambda": {
+ "hashes": [
+ "sha256:275297944c5e36a170b37ce70229f21db6dd3561606799f18d96e36ac5df6876",
+ "sha256:a12232002e04ee06b413b47068bc6bb085aeaa3693d28e9bf0efd76fa6953a0b"
+ ],
+ "version": "==1.34.46"
+ },
+ "mypy-boto3-rds": {
+ "hashes": [
+ "sha256:59124bd98653c73c685b7dc0d0a9069572d340f0ecb116a9706aa3e2d40a166d",
+ "sha256:9561dfac562ec9cd039806d5de2bc2bb8be4f9f7c03620270550a49e456fef46"
+ ],
+ "version": "==1.34.50"
+ },
+ "mypy-boto3-s3": {
+ "hashes": [
+ "sha256:71c39ab0623cdb442d225b71c1783f6a513cff4c4a13505a2efbb2e3aff2e965",
+ "sha256:f9669ecd182d5bf3532f5f2dcc5e5237776afe157ad5a0b37b26d6bec5fcc432"
+ ],
+ "version": "==1.34.14"
+ },
+ "mypy-boto3-secretsmanager": {
+ "hashes": [
+ "sha256:64e9df58f71072f0a912ecaca626683f4536da078caa204ac07928c4b1481b8b",
+ "sha256:abbf560775c2fe0dc383b7f70c16a1bf753d9b3ffc0caa5e35447e685783a68b"
+ ],
+ "version": "==1.34.43"
+ },
+ "mypy-boto3-sqs": {
+ "hashes": [
+ "sha256:0bf8995f58919ab295398100e72eaa7da898adcfd9d339a42f3c48ce473419d5",
+ "sha256:94d8aea4ae75605f70e58e440d706e04d5c614101ddb2f0c73d306d776d10995"
+ ],
+ "version": "==1.34.0"
+ },
+ "mypy-boto3-ssm": {
+ "hashes": [
+ "sha256:6517b1dc01e3ffe48a251c91e2a7fb6801223baf4a8cf1600411f9e132422297",
+ "sha256:be70cc32f9a07e6701746ebe65fba14d59c3f24a8511d275fd8322c9365f2270"
+ ],
+ "version": "==1.34.47"
+ },
+ "mypy-extensions": {
+ "hashes": [
+ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
+ "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==1.0.0"
+ },
+ "nodeenv": {
+ "hashes": [
+ "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2",
+ "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
+ "version": "==1.8.0"
+ },
+ "orderedmultidict": {
+ "hashes": [
+ "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad",
+ "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"
+ ],
+ "version": "==1.0.1"
+ },
+ "packaging": {
+ "hashes": [
+ "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
+ "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==20.9"
+ },
+ "pathspec": {
+ "hashes": [
+ "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
+ "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.12.1"
+ },
+ "pep517": {
+ "hashes": [
+ "sha256:1b2fa2ffd3938bb4beffe5d6146cbcb2bda996a5a4da9f31abffd8b24e07b317",
+ "sha256:31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.13.1"
+ },
+ "pip": {
+ "hashes": [
+ "sha256:ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc",
+ "sha256:ea9bd1a847e8c5774a5777bb398c19e80bcd4e2aa16a4b301b718fe6f593aba2"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==24.0"
+ },
+ "pip-shims": {
+ "hashes": [
+ "sha256:089e3586a92b1b8dbbc16b2d2859331dc1c412d3e3dbcd91d80e6b30d73db96c",
+ "sha256:2ae9f21c0155ca5c37d2734eb5f9a7d98c4c42a122d1ba3eddbacc9d9ea9fbae"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.7.3"
+ },
+ "pipenv-setup": {
+ "hashes": [
+ "sha256:0def7ec3363f58b38a43dc59b2078fcee67b47301fd51a41b8e34e6f79812b1a",
+ "sha256:6ceda7145a3088494d8ca68fded4b0473022dc62eb786a021c137632c44298b5"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2' and python_version < '4'",
+ "version": "==3.2.0"
+ },
+ "pipfile": {
+ "hashes": [
+ "sha256:f7d9f15de8b660986557eb3cc5391aa1a16207ac41bc378d03f414762d36c984"
+ ],
+ "version": "==0.0.2"
+ },
+ "platformdirs": {
+ "hashes": [
+ "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068",
+ "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.2.0"
+ },
+ "plette": {
+ "extras": [
+ "validation"
+ ],
+ "hashes": [
+ "sha256:12c51cd69e8e15d0bba9ea6028d9119cf143ebc418a1b6d2e7ae053db05eb768",
+ "sha256:a853b7a8f9e106c652a44ad356a88ac06c45036cc6ee01c6ba6165cfd752982c"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.0.0"
+ },
+ "pluggy": {
+ "hashes": [
+ "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981",
+ "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.4.0"
+ },
+ "pre-commit": {
+ "hashes": [
+ "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c",
+ "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==3.6.2"
+ },
+ "publication": {
+ "hashes": [
+ "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6",
+ "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"
+ ],
+ "version": "==0.0.3"
+ },
+ "pycln": {
+ "hashes": [
+ "sha256:1f3eefb7be18a9ee06c3bdd0ba2e91218cd39317e20130325f107e96eb84b9f6",
+ "sha256:d1bf648df17077306100815d255d45430035b36f66bac635df04a323c61ba126"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.7.0' and python_version < '4'",
+ "version": "==2.4.0"
+ },
+ "pycparser": {
+ "hashes": [
+ "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
+ "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
+ ],
+ "version": "==2.21"
+ },
+ "pylint": {
+ "hashes": [
+ "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74",
+ "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==3.1.0"
+ },
+ "pyparsing": {
+ "hashes": [
+ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
+ "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
+ ],
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==2.4.7"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd",
+ "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==8.0.2"
+ },
+ "pytest-cov": {
+ "hashes": [
+ "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6",
+ "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==4.1.0"
+ },
+ "pytest-mock": {
+ "hashes": [
+ "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f",
+ "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==3.12.0"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==2.8.2"
+ },
+ "pyyaml": {
+ "hashes": [
+ "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
+ "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+ "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
+ "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+ "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+ "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+ "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+ "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+ "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+ "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+ "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
+ "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
+ "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+ "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
+ "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+ "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+ "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+ "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+ "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+ "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+ "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+ "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
+ "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+ "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+ "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+ "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
+ "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
+ "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+ "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+ "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
+ "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+ "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+ "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+ "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+ "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+ "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+ "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+ "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+ "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+ "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+ "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+ "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+ "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
+ "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+ "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
+ "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+ "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+ "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+ "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+ "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+ "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==6.0.1"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
+ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==2.31.0"
+ },
+ "requirementslib": {
+ "hashes": [
+ "sha256:28924cf11a2fa91adb03f8431d80c2a8c3dc386f1c48fb2be9a58e4c39072354",
+ "sha256:d26ec6ad45e1ffce9532303543996c9c71a99dc65f783908f112e3f2aae7e49c"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.6.9"
+ },
+ "responses": {
+ "hashes": [
+ "sha256:01ae6a02b4f34e39bffceb0fc6786b67a25eae919c6368d05eabc8d9576c2a66",
+ "sha256:2f0b9c2b6437db4b528619a77e5d565e4ec2a9532162ac1a131a83529db7be1a"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.25.0"
+ },
+ "s3transfer": {
+ "hashes": [
+ "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e",
+ "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.10.0"
+ },
+ "setuptools": {
+ "hashes": [
+ "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56",
+ "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==69.1.1"
+ },
+ "six": {
+ "hashes": [
+ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+ "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==1.16.0"
+ },
+ "syrupy": {
+ "hashes": [
+ "sha256:203e52f9cb9fa749cf683f29bd68f02c16c3bc7e7e5fe8f2fc59bdfe488ce133",
+ "sha256:37a835c9ce7857eeef86d62145885e10b3cb9615bc6abeb4ce404b3f18e1bb36"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.8.1' and python_version < '4'",
+ "version": "==4.6.1"
+ },
+ "toml": {
+ "hashes": [
+ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+ "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==0.10.2"
+ },
+ "tomli": {
+ "hashes": [
+ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
+ "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==2.0.1"
+ },
+ "tomlkit": {
+ "hashes": [
+ "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b",
+ "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==0.12.4"
+ },
+ "typeguard": {
+ "hashes": [
+ "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4",
+ "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"
+ ],
+ "markers": "python_full_version >= '3.5.3'",
+ "version": "==2.13.3"
+ },
+ "typer": {
+ "hashes": [
+ "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2",
+ "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.9.0"
+ },
+ "types-awscrt": {
+ "hashes": [
+ "sha256:10245570c7285e949362b4ae710c54bf285d64a27453d42762477bcee5cd77a3",
+ "sha256:73be0a2720d6f76b924df6917d4edf4c9958f83e5c25bf7d9f0c1e9cdf836941"
+ ],
+ "markers": "python_version >= '3.7' and python_version < '4.0'",
+ "version": "==0.20.4"
+ },
+ "types-boto3": {
+ "hashes": [
+ "sha256:15f3ffad0314e40a0708fec25f94891414f93260202422bf8b19b6913853c983",
+ "sha256:a6a88e94d59d887839863a64095493956efc148e747206880a7eb47d90ae8398"
+ ],
+ "index": "pypi",
+ "version": "==1.0.2"
+ },
+ "types-pyyaml": {
+ "hashes": [
+ "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062",
+ "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"
+ ],
+ "index": "pypi",
+ "version": "==6.0.12.12"
+ },
+ "types-requests": {
+ "hashes": [
+ "sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b",
+ "sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==2.31.0.20240218"
+ },
+ "types-s3transfer": {
+ "hashes": [
+ "sha256:35e4998c25df7f8985ad69dedc8e4860e8af3b43b7615e940d53c00d413bdc69",
+ "sha256:44fcdf0097b924a9aab1ee4baa1179081a9559ca62a88c807e2b256893ce688f"
+ ],
+ "markers": "python_version >= '3.7' and python_version < '4.0'",
+ "version": "==0.10.0"
+ },
+ "types-setuptools": {
+ "hashes": [
+ "sha256:30a0d9903a81a424bd0f979534552a016a4543760aaffd499b9a2fe85bae0bfd",
+ "sha256:8a886a1fd06b668782dfbdaded4fd8a4e8c9f3d8d4c02acdd1240df098f50bf7"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==69.1.0.20240223"
+ },
+ "types-toml": {
+ "hashes": [
+ "sha256:58b0781c681e671ff0b5c0319309910689f4ab40e8a2431e205d70c94bb6efb1",
+ "sha256:61951da6ad410794c97bec035d59376ce1cbf4453dc9b6f90477e81e4442d631"
+ ],
+ "index": "pypi",
+ "version": "==0.10.8.7"
+ },
+ "types-urllib3": {
+ "hashes": [
+ "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f",
+ "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"
+ ],
+ "index": "pypi",
+ "version": "==1.26.25.14"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
+ "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.10.0"
+ },
+ "typing-inspect": {
+ "hashes": [
+ "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f",
+ "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"
+ ],
+ "version": "==0.9.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
+ "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
+ ],
+ "markers": "python_version >= '3.10'",
+ "version": "==2.0.7"
+ },
+ "virtualenv": {
+ "hashes": [
+ "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a",
+ "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==20.25.1"
+ },
+ "vistir": {
+ "hashes": [
+ "sha256:1a89a612fb667c26ed6b4ed415b01e0261e13200a350c43d1990ace0ef44d35b",
+ "sha256:a8beb7643d07779cdda3941a08dad77d48de94883dbd3cb2b9b5ecb7eb7c0994"
+ ],
+ "index": "pypi",
+ "markers": "python_version not in '3.0, 3.1, 3.2, 3.3' and python_version >= '3.7'",
+ "version": "==0.6.1"
+ },
+ "werkzeug": {
+ "hashes": [
+ "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc",
+ "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.0.1"
+ },
+ "wheel": {
+ "hashes": [
+ "sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d",
+ "sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==0.42.0"
+ },
+ "wrapt": {
+ "hashes": [
+ "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc",
+ "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81",
+ "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09",
+ "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e",
+ "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca",
+ "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0",
+ "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb",
+ "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487",
+ "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40",
+ "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c",
+ "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060",
+ "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202",
+ "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41",
+ "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9",
+ "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b",
+ "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664",
+ "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d",
+ "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362",
+ "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00",
+ "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc",
+ "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1",
+ "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267",
+ "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956",
+ "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966",
+ "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1",
+ "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228",
+ "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72",
+ "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d",
+ "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292",
+ "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0",
+ "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0",
+ "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36",
+ "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c",
+ "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5",
+ "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f",
+ "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73",
+ "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b",
+ "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2",
+ "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593",
+ "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39",
+ "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389",
+ "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf",
+ "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf",
+ "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89",
+ "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c",
+ "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c",
+ "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f",
+ "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440",
+ "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465",
+ "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136",
+ "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b",
+ "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8",
+ "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3",
+ "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8",
+ "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6",
+ "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e",
+ "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f",
+ "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c",
+ "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e",
+ "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8",
+ "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2",
+ "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020",
+ "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35",
+ "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d",
+ "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3",
+ "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537",
+ "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809",
+ "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d",
+ "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a",
+ "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==1.16.0"
+ },
+ "xmltodict": {
+ "hashes": [
+ "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56",
+ "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"
+ ],
+ "markers": "python_version >= '3.4'",
+ "version": "==0.13.0"
+ }
+ }
+}
diff --git a/templates/modules/cms_user_authentication_on_aws/v1/instance_infrastructure/source/tests/handlers/token_validation_lambda/__init__.py b/source/lib/__init__.py
similarity index 100%
rename from templates/modules/cms_user_authentication_on_aws/v1/instance_infrastructure/source/tests/handlers/token_validation_lambda/__init__.py
rename to source/lib/__init__.py
diff --git a/source/lib/cms_common/__init__.py b/source/lib/cms_common/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/lib/cms_common/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/lib/cms_common/aspects/__init__.py b/source/lib/cms_common/aspects/__init__.py
new file mode 100644
index 00000000..36cb8aa4
--- /dev/null
+++ b/source/lib/cms_common/aspects/__init__.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from .condition import ConditionAspect
+from .nag_suppression import NagSuppression, NagType
+from .vpc_aspect import (
+ ApplyVpcOnCustomResource,
+ generate_ec2_vpc_policy_cfn_format,
+ make_vpc_cfn_config,
+)
diff --git a/source/lib/cms_common/aspects/condition.py b/source/lib/cms_common/aspects/condition.py
new file mode 100644
index 00000000..f6a9427a
--- /dev/null
+++ b/source/lib/cms_common/aspects/condition.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+
+# Standard Library
+from typing import cast
+
+# Third Party Libraries
+import jsii
+
+# AWS Libraries
+from aws_cdk import CfnCondition, CfnResource, IAspect
+from constructs import IConstruct
+
+
+@jsii.implements(IAspect)
+class ConditionAspect:
+ def __init__(self, condition: CfnCondition) -> None:
+ self.condition = condition
+
+ # Visits every resource defined in the construct and applies the specified condition to the applicable resources.
+ def visit(self, node: IConstruct) -> None:
+ resource: CfnResource = cast(CfnResource, node)
+ if hasattr(resource, "cfn_options") and resource.cfn_options is not None:
+ resource.cfn_options.condition = self.condition
diff --git a/source/lib/cms_common/aspects/nag_suppression.py b/source/lib/cms_common/aspects/nag_suppression.py
new file mode 100644
index 00000000..82b4f3ee
--- /dev/null
+++ b/source/lib/cms_common/aspects/nag_suppression.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import json
+from enum import Enum
+from typing import Any, Dict, Optional
+
+# Third Party Libraries
+import jsii
+
+# AWS Libraries
+from aws_cdk import CfnResource, IAspect
+from constructs import IConstruct
+
+
+class NagType(Enum):
+ CDK_NAG = "cdk_nag"
+ CFN_NAG = "cfn_nag"
+
+
+@jsii.implements(IAspect)
+class NagSuppression:
+ def __init__(self, suppression_file_path: str, nag_type: NagType) -> None:
+ with open(suppression_file_path, encoding="UTF-8") as suppression_file:
+ self.suppressions = dict(json.loads(suppression_file.read()))
+ self.nag_type = nag_type
+
+ # Visits every resource defined in cfn template and applies suppression metadata by resource path from the suppresions file provided
+ # Resource paths in our suppression lists must be L1 constructs. When visiting an L2 construct, the path will not match
+ # and the resource will be skipped, however, the supporting L1 construct which eventually be visited, and the suppression will be added then
+ def visit(self, node: IConstruct) -> None:
+ node_path = f"/{node.node.path}"
+ suppression_metadata = self.suppressions.get(node_path)
+
+ if suppression_metadata:
+ CfnResource.add_metadata(
+ node, key=self.nag_type.value, value=suppression_metadata # type: ignore
+ )
+
+ @staticmethod
+ def add_inline_suppression(
+ node: Optional[IConstruct], suppression: Dict[str, Any], nag_type: NagType
+ ) -> None:
+ if node is not None:
+ CfnResource.add_metadata(node, key=nag_type.value, value=suppression) # type: ignore
diff --git a/source/lib/cms_common/aspects/tests/__init__.py b/source/lib/cms_common/aspects/tests/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/lib/cms_common/aspects/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/lib/cms_common/aspects/tests/test-cdk-nag-suppression-list.json b/source/lib/cms_common/aspects/tests/test-cdk-nag-suppression-list.json
new file mode 100644
index 00000000..a7b3652a
--- /dev/null
+++ b/source/lib/cms_common/aspects/tests/test-cdk-nag-suppression-list.json
@@ -0,0 +1,10 @@
+{
+ "/nag-test-stack/test-key/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "test-cdk-id",
+ "reason": "test-cdk-reason"
+ }
+ ]
+ }
+}
diff --git a/source/lib/cms_common/aspects/tests/test-cfn-nag-suppression-list.json b/source/lib/cms_common/aspects/tests/test-cfn-nag-suppression-list.json
new file mode 100644
index 00000000..ddba29bf
--- /dev/null
+++ b/source/lib/cms_common/aspects/tests/test-cfn-nag-suppression-list.json
@@ -0,0 +1,10 @@
+{
+ "/nag-test-stack/test-key/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "test-cfn-id",
+ "reason": "test-cfn-reason"
+ }
+ ]
+ }
+}
diff --git a/source/lib/cms_common/aspects/tests/test_condition.py b/source/lib/cms_common/aspects/tests/test_condition.py
new file mode 100644
index 00000000..5bec1eff
--- /dev/null
+++ b/source/lib/cms_common/aspects/tests/test_condition.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from typing import Any
+
+# AWS Libraries
+from aws_cdk import App, Aspects, CfnCondition, Stack, assertions, aws_kms
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..condition import ConditionAspect
+
+
+class AspectTestStack(Stack):
+ def __init__(
+ self, scope: Construct, construct_id: str, add_condition: bool, **kwargs: Any
+ ) -> None:
+ super().__init__(scope, construct_id, **kwargs)
+
+ self.test_key = aws_kms.Key(
+ self,
+ "test-key",
+ enable_key_rotation=True,
+ )
+ cfn_condition = CfnCondition(
+ self,
+ "test-condition",
+ )
+ if add_condition:
+ Aspects.of(self.test_key).add(ConditionAspect(cfn_condition))
+
+
+def test_condition_aspect_true() -> None:
+ app = App()
+ test_stack = AspectTestStack(app, "condition-test-stack", add_condition=True)
+
+ template = assertions.Template.from_stack(test_stack)
+ template.has_resource("AWS::KMS::Key", {"Condition": "testcondition"})
+
+
+def test_condition_aspect_false() -> None:
+ app = App()
+ test_stack = AspectTestStack(app, "condition-test-stack", add_condition=False)
+
+ template = assertions.Template.from_stack(test_stack)
+ template.has_resource("AWS::KMS::Key", {"Condition": None})
diff --git a/source/lib/cms_common/aspects/tests/test_nag_suppression.py b/source/lib/cms_common/aspects/tests/test_nag_suppression.py
new file mode 100644
index 00000000..7f260139
--- /dev/null
+++ b/source/lib/cms_common/aspects/tests/test_nag_suppression.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from os.path import dirname, realpath
+from typing import Any
+
+# AWS Libraries
+from aws_cdk import App, Stack, assertions, aws_kms
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..nag_suppression import NagSuppression, NagType
+
+
+class NagTestStack(Stack):
+ def __init__(self, scope: Construct, construct_id: str, **kwargs: Any) -> None:
+ super().__init__(scope, construct_id, **kwargs)
+
+ self.test_key = aws_kms.Key(
+ self,
+ "test-key",
+ enable_key_rotation=True,
+ )
+
+
+def test_nag_suppression_cdk_metadata() -> None:
+ app = App()
+ test_stack = NagTestStack(app, "nag-test-stack")
+ cdk_nag_suppression = NagSuppression(
+ f"{dirname(realpath(__file__))}/test-cdk-nag-suppression-list.json",
+ NagType.CDK_NAG,
+ )
+ l1_construct = test_stack.test_key.node.default_child
+ if l1_construct is not None:
+ cdk_nag_suppression.visit(l1_construct)
+ template = assertions.Template.from_stack(test_stack)
+ template.has_resource(
+ "AWS::KMS::Key",
+ {
+ "Metadata": {
+ "cdk_nag": {
+ "rules_to_suppress": [
+ {"id": "test-cdk-id", "reason": "test-cdk-reason"}
+ ]
+ }
+ }
+ },
+ )
+ else:
+ assert False
+
+
+def test_nag_suppression_cfn_metadata() -> None:
+ app = App()
+ test_stack = NagTestStack(app, "nag-test-stack")
+ cfn_nag_suppression = NagSuppression(
+ f"{dirname(realpath(__file__))}/test-cfn-nag-suppression-list.json",
+ NagType.CFN_NAG,
+ )
+
+ l1_construct = test_stack.test_key.node.default_child
+ if l1_construct is not None:
+ cfn_nag_suppression.visit(l1_construct)
+ template = assertions.Template.from_stack(test_stack)
+ template.has_resource(
+ "AWS::KMS::Key",
+ {
+ "Metadata": {
+ "cfn_nag": {
+ "rules_to_suppress": [
+ {"id": "test-cfn-id", "reason": "test-cfn-reason"}
+ ]
+ }
+ }
+ },
+ )
+ else:
+ assert False
+
+
+def test_nag_suppression_inline_cdk_metadata() -> None:
+ app = App()
+ test_stack = NagTestStack(app, "nag-test-stack")
+ NagSuppression.add_inline_suppression(
+ node=test_stack.test_key.node.default_child,
+ suppression={
+ "rules_to_suppress": [{"id": "test-cdk-id", "reason": "test-cdk-reason"}]
+ },
+ nag_type=NagType.CDK_NAG,
+ )
+
+ template = assertions.Template.from_stack(test_stack)
+ template.has_resource(
+ "AWS::KMS::Key",
+ {
+ "Metadata": {
+ "cdk_nag": {
+ "rules_to_suppress": [
+ {"id": "test-cdk-id", "reason": "test-cdk-reason"}
+ ]
+ }
+ }
+ },
+ )
+
+
+def test_nag_suppression_inline_cfn_metadata() -> None:
+ app = App()
+ test_stack = NagTestStack(app, "nag-test-stack")
+ NagSuppression.add_inline_suppression(
+ node=test_stack.test_key.node.default_child,
+ suppression={
+ "rules_to_suppress": [{"id": "test-cfn-id", "reason": "test-cfn-reason"}]
+ },
+ nag_type=NagType.CFN_NAG,
+ )
+
+ template = assertions.Template.from_stack(test_stack)
+ template.has_resource(
+ "AWS::KMS::Key",
+ {
+ "Metadata": {
+ "cfn_nag": {
+ "rules_to_suppress": [
+ {"id": "test-cfn-id", "reason": "test-cfn-reason"}
+ ]
+ }
+ }
+ },
+ )
diff --git a/source/lib/cms_common/aspects/tests/test_vpc_custom_resource.py b/source/lib/cms_common/aspects/tests/test_vpc_custom_resource.py
new file mode 100644
index 00000000..41e7c041
--- /dev/null
+++ b/source/lib/cms_common/aspects/tests/test_vpc_custom_resource.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+
+# Connected Mobility Solution on AWS
+from ..vpc_aspect import generate_ec2_vpc_policy_cfn_format, make_vpc_cfn_config
+
+
+def test_make_vpc_cfn_config() -> None:
+ assert make_vpc_cfn_config(
+ security_group_logical_ids=["test-sec-group-1", "test-sec-group-2"],
+ subnet_names=["test-subnet-1", "test-subnet-2"],
+ ) == {
+ "SecurityGroupIds": [
+ {
+ "Fn::GetAtt": [
+ "test-sec-group-1",
+ "GroupId",
+ ]
+ },
+ {
+ "Fn::GetAtt": [
+ "test-sec-group-2",
+ "GroupId",
+ ]
+ },
+ ],
+ "SubnetIds": ["test-subnet-1", "test-subnet-2"],
+ }
+
+
+def test_generate_ec2_vpc_policy_cfn_format() -> None:
+ assert generate_ec2_vpc_policy_cfn_format(
+ subnet_names=["test-subnet-1", "test-subnet-2"]
+ ) == {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "ec2:CreateNetworkInterfacePermission",
+ ],
+ "Condition": {
+ "StringEquals": {
+ "ec2:Subnet": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {"Ref": "AWS::Partition"},
+ ":ec2:",
+ {"Ref": "AWS::Region"},
+ ":",
+ {"Ref": "AWS::AccountId"},
+ ":subnet/",
+ "test-subnet-1",
+ ],
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {"Ref": "AWS::Partition"},
+ ":ec2:",
+ {"Ref": "AWS::Region"},
+ ":",
+ {"Ref": "AWS::AccountId"},
+ ":subnet/",
+ "test-subnet-2",
+ ],
+ ]
+ },
+ ],
+ "ec2:AuthorizedService": "lambda.amazonaws.com",
+ }
+ },
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {"Ref": "AWS::Partition"},
+ ":ec2:",
+ {"Ref": "AWS::Region"},
+ ":",
+ {"Ref": "AWS::AccountId"},
+ ":network-interface/*",
+ ],
+ ]
+ },
+ },
+ {
+ "Action": [
+ "ec2:DescribeNetworkInterfaces",
+ "ec2:CreateNetworkInterface",
+ "ec2:DeleteNetworkInterface",
+ ],
+ "Effect": "Allow",
+ "Resource": "*",
+ },
+ ],
+ "Version": "2012-10-17",
+ },
+ "PolicyName": "ec2-policy",
+ }
diff --git a/source/lib/cms_common/aspects/vpc_aspect.py b/source/lib/cms_common/aspects/vpc_aspect.py
new file mode 100644
index 00000000..0d87ba00
--- /dev/null
+++ b/source/lib/cms_common/aspects/vpc_aspect.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import re
+from typing import Any, Dict, List
+
+# Third Party Libraries
+import jsii
+
+# AWS Libraries
+from aws_cdk import CfnResource, IAspect
+from constructs import IConstruct
+
+
+def make_vpc_cfn_config(
+ security_group_logical_ids: List[str], subnet_names: List[str]
+) -> Dict[str, Any]:
+ return {
+ "SecurityGroupIds": [
+ {
+ "Fn::GetAtt": [
+ security_group_logical_id,
+ "GroupId",
+ ]
+ }
+ for security_group_logical_id in security_group_logical_ids
+ ],
+ "SubnetIds": subnet_names,
+ }
+
+
+def generate_ec2_vpc_policy_cfn_format(subnet_names: List[str]) -> Dict[str, Any]:
+ return {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "ec2:CreateNetworkInterfacePermission",
+ ],
+ "Condition": {
+ "StringEquals": {
+ "ec2:Subnet": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {"Ref": "AWS::Partition"},
+ ":ec2:",
+ {"Ref": "AWS::Region"},
+ ":",
+ {"Ref": "AWS::AccountId"},
+ ":subnet/",
+ subnet_name,
+ ],
+ ]
+ }
+ for subnet_name in subnet_names
+ ],
+ "ec2:AuthorizedService": "lambda.amazonaws.com",
+ }
+ },
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {"Ref": "AWS::Partition"},
+ ":ec2:",
+ {"Ref": "AWS::Region"},
+ ":",
+ {"Ref": "AWS::AccountId"},
+ ":network-interface/*",
+ ],
+ ]
+ },
+ },
+ {
+ "Action": [
+ "ec2:DescribeNetworkInterfaces",
+ "ec2:CreateNetworkInterface",
+ "ec2:DeleteNetworkInterface",
+ ],
+ "Effect": "Allow",
+ "Resource": "*",
+ },
+ ],
+ "Version": "2012-10-17",
+ },
+ "PolicyName": "ec2-policy",
+ }
+
+
+@jsii.implements(IAspect)
+class ApplyVpcOnCustomResource:
+ def __init__(
+ self,
+ module_name: str,
+ security_group_logical_ids: List[str],
+ subnet_names: List[str],
+ ) -> None:
+ self.vpc_config = make_vpc_cfn_config(
+ security_group_logical_ids=security_group_logical_ids,
+ subnet_names=subnet_names,
+ )
+
+ self.ec2_cfn_policy = generate_ec2_vpc_policy_cfn_format(
+ subnet_names=subnet_names
+ )
+
+ self.service_resource_patterns = [
+ rf"^/{module_name}/LogRetention[a-zA-Z0-9]+/Resource$",
+ rf"^/{module_name}/AWS[a-zA-Z0-9]+/Resource$",
+ ]
+
+ self.service_role_patterns = [
+ rf"^/{module_name}/LogRetention[a-zA-Z0-9]+/ServiceRole/Resource$",
+ rf"^/{module_name}/AWS[a-zA-Z0-9]+/ServiceRole/Resource$",
+ ]
+
+ def visit(
+ self,
+ node: IConstruct,
+ ) -> None:
+ node_path = f"/{node.node.path}"
+ vpc_config_property_path = "VpcConfig"
+ policy_path = "Policies"
+
+ if any(
+ re.match(pattern, node_path) is not None
+ for pattern in self.service_resource_patterns
+ ):
+ CfnResource.add_property_override(
+ node, vpc_config_property_path, self.vpc_config # type: ignore[arg-type]
+ )
+ elif any(
+ re.match(pattern, node_path) is not None
+ for pattern in self.service_role_patterns
+ ):
+ CfnResource.add_property_override(
+ node, # type: ignore[arg-type]
+ policy_path,
+ [self.ec2_cfn_policy],
+ )
diff --git a/source/lib/cms_common/auth/__init__.py b/source/lib/cms_common/auth/__init__.py
new file mode 100644
index 00000000..488c618b
--- /dev/null
+++ b/source/lib/cms_common/auth/__init__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from .auth_configs import (
+ get_authorization_code_flow_config,
+ get_client_config,
+ get_idp_config,
+)
diff --git a/source/lib/cms_common/auth/auth_configs.py b/source/lib/cms_common/auth/auth_configs.py
new file mode 100644
index 00000000..9b9c9c64
--- /dev/null
+++ b/source/lib/cms_common/auth/auth_configs.py
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import json
+from dataclasses import dataclass
+from functools import lru_cache
+from typing import TYPE_CHECKING, List, Optional, Union, overload
+
+# Third Party Libraries
+from cattrs import ClassValidationError, structure
+
+# AWS Libraries
+import boto3
+from botocore.config import Config
+from botocore.exceptions import ClientError
+
+# Connected Mobility Solution on AWS
+from ..resource_names.auth import AuthResourceNames
+
+if TYPE_CHECKING:
+ # Third Party Libraries
+ from mypy_boto3_secretsmanager import SecretsManagerClient
+ from mypy_boto3_ssm import SSMClient
+else:
+ SecretsManagerClient = object
+ SSMClient = object
+
+
+class AuthConfigError(Exception):
+ def __init__(
+ self,
+ message: str = "Could not retrieve or parse auth configurations.",
+ code: int = 500,
+ ):
+ self.message = message
+ self.code = code
+
+
+@dataclass(frozen=True)
+class CMSIdPConfig:
+ iss_domain: str
+ alternate_aud_key: Optional[str]
+ auds: List[str]
+ scopes: List[str]
+
+ def __hash__(self) -> int:
+ auds_tuple = tuple(sorted(self.auds))
+ scopes_tuple = tuple(sorted(self.scopes))
+ return hash((self.iss_domain, self.alternate_aud_key, auds_tuple, scopes_tuple))
+
+
+@dataclass(frozen=True)
+class CMSClientConfig:
+ audience: str
+ token_endpoint: str
+ client_id: str
+ client_secret: str
+
+
+@dataclass(frozen=True)
+class CMSAuthorizationCodeFlowConfig:
+ token_endpoint: str
+ client_id: str
+ client_secret: str
+
+
+MAX_CACHE_SIZE_BOTO_CLIENT = 10
+MAX_CACHE_SIZE_AUTH_CONFIG = 100
+
+
+@lru_cache(maxsize=MAX_CACHE_SIZE_BOTO_CLIENT)
+def _get_secrets_manager_client(user_agent_string: str) -> SecretsManagerClient:
+ return boto3.client(
+ "secretsmanager",
+ config=Config(user_agent_extra=user_agent_string),
+ )
+
+
+@lru_cache(maxsize=MAX_CACHE_SIZE_BOTO_CLIENT)
+def _get_ssm_client(user_agent_string: str) -> SSMClient:
+ return boto3.client(
+ "ssm",
+ config=Config(user_agent_extra=user_agent_string),
+ )
+
+
+@lru_cache(maxsize=MAX_CACHE_SIZE_AUTH_CONFIG)
+def _get_auth_resource_names(identity_provider_id: str) -> AuthResourceNames:
+ return AuthResourceNames.from_identity_provider_id(identity_provider_id)
+
+
+# Config getter functions
+def get_idp_config(
+ user_agent_string: str,
+ identity_provider_id: str,
+) -> CMSIdPConfig:
+ auth_resource_names = _get_auth_resource_names(identity_provider_id)
+ idp_config_ssm_name = auth_resource_names.idp_config_secret_arn_ssm_parameter
+ return _get_config(
+ user_agent_string=user_agent_string,
+ ssm_name=idp_config_ssm_name,
+ config_dataclass_type=CMSIdPConfig,
+ )
+
+
+def get_client_config(
+ user_agent_string: str,
+ identity_provider_id: str,
+) -> CMSClientConfig:
+ auth_resource_names = _get_auth_resource_names(identity_provider_id)
+ client_config_ssm_name = auth_resource_names.client_config_secret_arn_ssm_parameter
+ return _get_config(
+ user_agent_string=user_agent_string,
+ ssm_name=client_config_ssm_name,
+ config_dataclass_type=CMSClientConfig,
+ )
+
+
+def get_authorization_code_flow_config(
+ user_agent_string: str,
+ identity_provider_id: str,
+) -> CMSAuthorizationCodeFlowConfig:
+ auth_resource_names = _get_auth_resource_names(identity_provider_id)
+ authorization_code_flow_config_ssm_name = (
+ auth_resource_names.authorization_code_flow_config_secret_arn_ssm_parameter
+ )
+ return _get_config(
+ user_agent_string=user_agent_string,
+ ssm_name=authorization_code_flow_config_ssm_name,
+ config_dataclass_type=CMSAuthorizationCodeFlowConfig,
+ )
+
+
+# Overloads necessary for mypy
+@overload
+def _get_config(
+ user_agent_string: str,
+ ssm_name: str,
+ config_dataclass_type: type[CMSIdPConfig],
+) -> CMSIdPConfig:
+ ...
+
+
+@overload
+def _get_config(
+ user_agent_string: str,
+ ssm_name: str,
+ config_dataclass_type: type[CMSClientConfig],
+) -> CMSClientConfig:
+ ...
+
+
+@overload
+def _get_config(
+ user_agent_string: str,
+ ssm_name: str,
+ config_dataclass_type: type[CMSAuthorizationCodeFlowConfig],
+) -> CMSAuthorizationCodeFlowConfig:
+ ...
+
+
+# Helper function to dynamically get the right config based on SSM path. Each config has an SSM parameter to expose the config secret Arn.
+def _get_config(
+ user_agent_string: str,
+ ssm_name: str,
+ config_dataclass_type: Union[
+ type[CMSIdPConfig], type[CMSClientConfig], type[CMSAuthorizationCodeFlowConfig]
+ ],
+) -> Union[CMSIdPConfig, CMSClientConfig, CMSAuthorizationCodeFlowConfig]:
+ try:
+ config_secret_arn = _get_ssm_client(user_agent_string).get_parameter(
+ Name=ssm_name
+ )["Parameter"]["Value"]
+
+ config_secret_value = _get_secrets_manager_client(
+ user_agent_string
+ ).get_secret_value(SecretId=config_secret_arn)["SecretString"]
+
+ config_object = json.loads(config_secret_value)
+
+ try:
+ config_dataclass: Union[CMSIdPConfig, CMSClientConfig] = structure(obj=config_object, cl=config_dataclass_type) # type: ignore[assignment]
+ except ClassValidationError as e:
+ raise AuthConfigError(
+ "Auth Config Error: error while converting the auth config into the expected data format. Ensure your secret value matches the expected format."
+ ) from e
+ except json.JSONDecodeError as e:
+ raise AuthConfigError(
+ "Auth Config Error: JSON error while decoding the auth config secret."
+ ) from e
+ except ClientError as e:
+ raise AuthConfigError(
+ "Auth Config Error: client error while retrieving the secret or ssm parameter from the AWS account."
+ ) from e
+ except KeyError as e:
+ raise AuthConfigError(
+ "Auth Config Error: unexpected response from Secrets Manager get_secret_value. Missing expected 'SecretString' key."
+ ) from e
+ return config_dataclass
diff --git a/source/lib/cms_common/auth/tests/__init__.py b/source/lib/cms_common/auth/tests/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/lib/cms_common/auth/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/lib/cms_common/auth/tests/fixture_auth.py b/source/lib/cms_common/auth/tests/fixture_auth.py
new file mode 100644
index 00000000..94be28b1
--- /dev/null
+++ b/source/lib/cms_common/auth/tests/fixture_auth.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+# mypy: disable-error-code="name-defined"
+
+# Standard Library
+import json
+from typing import TYPE_CHECKING, Callable, Dict, List
+
+# Third Party Libraries
+import pytest
+from moto import mock_aws
+
+# AWS Libraries
+import boto3
+
+# Connected Mobility Solution on AWS
+from ...resource_names.auth import AuthResourceNames
+
+TEST_USER_AGENT_STRING = "test-user-agent-string"
+TEST_IDENTITY_PROVIDER_ID = "test_idp"
+TEST_AUTH_RESOURCE_NAMES_CLASS = AuthResourceNames.from_identity_provider_id(
+ TEST_IDENTITY_PROVIDER_ID
+)
+
+if TYPE_CHECKING:
+ # Third Party Libraries
+ from mypy_boto3_secretsmanager import SecretsManagerClient
+ from mypy_boto3_ssm import SSMClient
+else:
+ SecretsManagerClient = object
+ SSMClient = object
+
+# IDP CONFIG
+@pytest.fixture(name="idp_config_secret_string_valid", scope="module")
+def fixture_idp_config_secret_string_valid() -> Dict[str, str | List[str]]:
+ idp_config_json: Dict[str, str | List[str]] = {
+ "iss_domain": "TEST_ISS_DOMAIN",
+ "alternate_aud_key": "TEST_ALTERNATE_AUD_KEY",
+ "auds": [
+ "TEST_USER_CLIENT_ID",
+ "TEST_SERVICE_CLIENT_ID",
+ ],
+ "scopes": ["TEST_USER_SCOPE", "TEST_SERVICE_SCOPE"],
+ }
+ return idp_config_json
+
+
+@pytest.fixture(name="mock_idp_config_valid")
+def fixture_mock_idp_config_valid(
+ idp_config_secret_string_valid: str,
+) -> Callable[[], None]:
+ @mock_aws
+ def moto_boto() -> None:
+ secretsmanager_client: SecretsManagerClient = boto3.client("secretsmanager")
+ secret_arn = secretsmanager_client.create_secret(
+ Name=TEST_AUTH_RESOURCE_NAMES_CLASS.idp_config_secret,
+ SecretString=json.dumps(idp_config_secret_string_valid),
+ )["ARN"]
+
+ ssm_client: SSMClient = boto3.client("ssm")
+ ssm_client.put_parameter(
+ Name=TEST_AUTH_RESOURCE_NAMES_CLASS.idp_config_secret_arn_ssm_parameter,
+ Value=secret_arn,
+ Type="String",
+ )
+
+ return moto_boto
+
+
+@pytest.fixture(name="mock_idp_config_invalid_json")
+def fixture_mock_idp_config_invalid_json() -> Callable[[], None]:
+ @mock_aws
+ def moto_boto() -> None:
+ secretsmanager_client: SecretsManagerClient = boto3.client("secretsmanager")
+ secret_arn = secretsmanager_client.create_secret(
+ Name=TEST_AUTH_RESOURCE_NAMES_CLASS.idp_config_secret,
+ SecretString="Not a valid json string",
+ )["ARN"]
+
+ ssm_client: SSMClient = boto3.client("ssm")
+ ssm_client.put_parameter(
+ Name=TEST_AUTH_RESOURCE_NAMES_CLASS.idp_config_secret_arn_ssm_parameter,
+ Value=secret_arn,
+ Type="String",
+ )
+
+ return moto_boto
+
+
+@pytest.fixture(name="mock_idp_config_invalid_data_format")
+def fixture_mock_idp_config_invalid_data_format() -> Callable[[], None]:
+ @mock_aws
+ def moto_boto() -> None:
+ secretsmanager_client: SecretsManagerClient = boto3.client("secretsmanager")
+ secret_arn = secretsmanager_client.create_secret(
+ Name=TEST_AUTH_RESOURCE_NAMES_CLASS.idp_config_secret,
+ SecretString=json.dumps({"incorrect_key": "value"}),
+ )["ARN"]
+
+ ssm_client: SSMClient = boto3.client("ssm")
+ ssm_client.put_parameter(
+ Name=TEST_AUTH_RESOURCE_NAMES_CLASS.idp_config_secret_arn_ssm_parameter,
+ Value=secret_arn,
+ Type="String",
+ )
+
+ return moto_boto
+
+
+# CLIENT CONFIG
+@pytest.fixture(name="client_config_secret_string_valid", scope="module")
+def fixture_client_config_secret_string_valid() -> dict[str, str]:
+ client_config_json: dict[str, str] = {
+ "audience": "",
+ "token_endpoint": "TEST_TOKEN_ENDPOINT",
+ "client_id": "TEST_CLIENT_ID",
+ "client_secret": "TEST_CLIENT_SECRET",
+ }
+ return client_config_json
+
+
+@pytest.fixture(name="mock_client_config_valid")
+def fixture_mock_client_config_valid(
+ client_config_secret_string_valid: str,
+) -> Callable[[], None]:
+ @mock_aws
+ def moto_boto() -> None:
+ secretsmanager_client: SecretsManagerClient = boto3.client("secretsmanager")
+ secret_arn = secretsmanager_client.create_secret(
+ Name=TEST_AUTH_RESOURCE_NAMES_CLASS.client_config_secret,
+ SecretString=json.dumps(client_config_secret_string_valid),
+ )["ARN"]
+
+ ssm_client: SSMClient = boto3.client("ssm")
+ ssm_client.put_parameter(
+ Name=TEST_AUTH_RESOURCE_NAMES_CLASS.client_config_secret_arn_ssm_parameter,
+ Value=secret_arn,
+ Type="String",
+ )
+
+ return moto_boto
+
+
+# AUTHORIZATION CODE FLOW CONFIG
+@pytest.fixture(
+ name="authorization_code_flow_config_secret_string_valid", scope="module"
+)
+def fixture_authorization_code_flow_config_secret_string_valid() -> dict[str, str]:
+ client_config_json: dict[str, str] = {
+ "token_endpoint": "TEST_TOKEN_ENDPOINT",
+ "client_id": "TEST_CLIENT_ID",
+ "client_secret": "TEST_CLIENT_SECRET",
+ }
+ return client_config_json
+
+
+@pytest.fixture(name="mock_authorization_code_flow_config_valid")
+def fixture_mock_authorization_code_flow_config_valid(
+ authorization_code_flow_config_secret_string_valid: str,
+) -> Callable[[], None]:
+ @mock_aws
+ def moto_boto() -> None:
+ secretsmanager_client: SecretsManagerClient = boto3.client("secretsmanager")
+ secret_arn = secretsmanager_client.create_secret(
+ Name=TEST_AUTH_RESOURCE_NAMES_CLASS.authorization_code_flow_config_secret,
+ SecretString=json.dumps(authorization_code_flow_config_secret_string_valid),
+ )["ARN"]
+
+ ssm_client: SSMClient = boto3.client("ssm")
+ ssm_client.put_parameter(
+ Name=TEST_AUTH_RESOURCE_NAMES_CLASS.authorization_code_flow_config_secret_arn_ssm_parameter,
+ Value=secret_arn,
+ Type="String",
+ )
+
+ return moto_boto
diff --git a/source/lib/cms_common/auth/tests/test_auth_configs.py b/source/lib/cms_common/auth/tests/test_auth_configs.py
new file mode 100644
index 00000000..c0b45d39
--- /dev/null
+++ b/source/lib/cms_common/auth/tests/test_auth_configs.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from typing import Callable, Dict, List, Tuple
+
+# Third Party Libraries
+import pytest
+from moto import mock_aws
+
+# Connected Mobility Solution on AWS
+from ..auth_configs import (
+ AuthConfigError,
+ CMSAuthorizationCodeFlowConfig,
+ CMSClientConfig,
+ CMSIdPConfig,
+ get_authorization_code_flow_config,
+ get_client_config,
+ get_idp_config,
+)
+from .fixture_auth import TEST_IDENTITY_PROVIDER_ID, TEST_USER_AGENT_STRING
+
+
+@mock_aws
+def test_get_idp_config_success(
+ idp_config_secret_string_valid: Dict[str, str | List[str]],
+ mock_idp_config_valid: Callable[[], None],
+) -> None:
+ mock_idp_config_valid()
+ idp_config = get_idp_config(TEST_USER_AGENT_STRING, TEST_IDENTITY_PROVIDER_ID)
+ assert isinstance(idp_config, CMSIdPConfig)
+ assert idp_config.iss_domain == idp_config_secret_string_valid["iss_domain"]
+ assert (
+ idp_config.alternate_aud_key
+ == idp_config_secret_string_valid["alternate_aud_key"]
+ )
+ assert idp_config.auds == idp_config_secret_string_valid["auds"]
+ assert idp_config.scopes == idp_config_secret_string_valid["scopes"]
+
+
+def test_get_idp_config_client_error() -> None:
+ with pytest.raises(
+ AuthConfigError,
+ match=r"Auth Config Error: client error while retrieving the secret or ssm parameter from the AWS account.",
+ ):
+ get_idp_config(TEST_USER_AGENT_STRING, TEST_IDENTITY_PROVIDER_ID)
+
+
+@mock_aws
+def test_get_idp_config_json_decode_error(
+ mock_idp_config_invalid_json: Callable[[], None],
+) -> None:
+ mock_idp_config_invalid_json()
+ with pytest.raises(
+ AuthConfigError,
+ match=r"Auth Config Error: JSON error while decoding the auth config secret.",
+ ):
+ get_idp_config(
+ TEST_USER_AGENT_STRING,
+ TEST_IDENTITY_PROVIDER_ID,
+ )
+
+
+@mock_aws
+def test_get_idp_config_class_validation_error(
+ mock_idp_config_invalid_data_format: Callable[[], None],
+) -> None:
+ mock_idp_config_invalid_data_format()
+ with pytest.raises(
+ AuthConfigError,
+ match=r"Auth Config Error: error while converting the auth config into the expected data format. Ensure your secret value matches the expected format.",
+ ):
+ get_idp_config(
+ TEST_USER_AGENT_STRING,
+ TEST_IDENTITY_PROVIDER_ID,
+ )
+
+
+@mock_aws
+def test_get_client_config_success(
+ client_config_secret_string_valid: dict[str, str | Tuple[str, ...]],
+ mock_client_config_valid: Callable[[], None],
+) -> None:
+ mock_client_config_valid()
+ client_config = get_client_config(TEST_USER_AGENT_STRING, TEST_IDENTITY_PROVIDER_ID)
+ assert isinstance(client_config, CMSClientConfig)
+ assert client_config.audience == client_config_secret_string_valid["audience"]
+ assert (
+ client_config.token_endpoint
+ == client_config_secret_string_valid["token_endpoint"]
+ )
+ assert client_config.client_id == client_config_secret_string_valid["client_id"]
+ assert (
+ client_config.client_secret
+ == client_config_secret_string_valid["client_secret"]
+ )
+
+
+@mock_aws
+def test_get_authorization_code_flow_config_success(
+ authorization_code_flow_config_secret_string_valid: dict[
+ str, str | Tuple[str, ...]
+ ],
+ mock_authorization_code_flow_config_valid: Callable[[], None],
+) -> None:
+ mock_authorization_code_flow_config_valid()
+ authorization_code_flow_config = get_authorization_code_flow_config(
+ TEST_USER_AGENT_STRING, TEST_IDENTITY_PROVIDER_ID
+ )
+ assert isinstance(authorization_code_flow_config, CMSAuthorizationCodeFlowConfig)
+ assert (
+ authorization_code_flow_config.token_endpoint
+ == authorization_code_flow_config_secret_string_valid["token_endpoint"]
+ )
+ assert (
+ authorization_code_flow_config.client_id
+ == authorization_code_flow_config_secret_string_valid["client_id"]
+ )
+ assert (
+ authorization_code_flow_config.client_secret
+ == authorization_code_flow_config_secret_string_valid["client_secret"]
+ )
diff --git a/source/lib/cms_common/boto3_wrappers/__init__.py b/source/lib/cms_common/boto3_wrappers/__init__.py
new file mode 100644
index 00000000..a3e7ff7c
--- /dev/null
+++ b/source/lib/cms_common/boto3_wrappers/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from .dynamo_crud import DynHelpers
diff --git a/source/lib/cms_common/boto3_wrappers/dynamo_crud.py b/source/lib/cms_common/boto3_wrappers/dynamo_crud.py
new file mode 100644
index 00000000..3aecae72
--- /dev/null
+++ b/source/lib/cms_common/boto3_wrappers/dynamo_crud.py
@@ -0,0 +1,232 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import os
+import time
+from typing import Any, Dict, Generator, List, Optional
+
+# AWS Libraries
+import boto3
+from aws_lambda_powertools import Logger, Tracer
+from botocore.config import Config
+from botocore.exceptions import ClientError
+
+tracer = Tracer()
+logger = Logger()
+
+
+class DynHelpers:
+ dynamo_object = None
+ MAX_ITEM_PER_BATCH_IN_BATCH_WRITE = 25
+
+ @staticmethod
+ def dyn_resource() -> Any:
+ if getattr(DynHelpers, "dynamo_object"):
+ return DynHelpers.dynamo_object
+
+ DynHelpers.dynamo_object = boto3.resource(
+ "dynamodb",
+ region_name=os.environ.get("REGION_NAME"),
+ config=Config(user_agent_extra=os.environ["USER_AGENT_STRING"]),
+ )
+ return DynHelpers.dynamo_object
+
+ @staticmethod
+ def get_all(*args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
+ return [
+ item for result in DynHelpers.dyn_scan(*args, **kwargs) for item in result
+ ]
+
+ @staticmethod
+ def put_item(table_name: str, item: Dict[str, Any]) -> None:
+ if not item.get("timestamp"):
+ item["timestamp"] = str(time.time())
+
+ try:
+ DynHelpers.dyn_resource().Table(table_name).put_item(Item=item)
+ except ClientError as err:
+ logger.error(
+ "Couldn't update item %s to table %s. Here's why: %s: %s",
+ item,
+ table_name,
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ @staticmethod
+ def get_item(table_name: str, get_criteria: Dict[str, Any]) -> Any:
+ try:
+ response = (
+ DynHelpers.dyn_resource().Table(table_name).get_item(Key=get_criteria)
+ )
+ return response["Item"]
+ except ClientError as err:
+ logger.error(
+ "Couldn't get item %s from table %s. Here's why: %s: %s",
+ get_criteria,
+ table_name,
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+ except KeyError:
+ logger.error(
+ "Item %s not found in table %s.",
+ get_criteria,
+ table_name,
+ exc_info=True,
+ )
+ raise
+
+ @staticmethod
+ def update_item(
+ table_name: str,
+ item: Dict[str, Any],
+ update_expression: Optional[str] = None,
+ expression_attr: Optional[Dict[str, Any]] = None,
+ return_values: str = "UPDATED_NEW",
+ ) -> Any:
+ try:
+ response = (
+ DynHelpers.dyn_resource()
+ .Table(table_name)
+ .update_item(
+ Key=item,
+ UpdateExpression=update_expression,
+ ExpressionAttributeValues=expression_attr,
+ ReturnValues=return_values,
+ )
+ )
+ except ClientError as err:
+ logger.error(
+ "Couldn't update item %s to table %s. Here's why: %s: %s",
+ item,
+ table_name,
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ return response["Attributes"]
+
+ @staticmethod
+ def delete_item(table_name: str, delete_keys: dict[str, Any]) -> None:
+ try:
+ DynHelpers.dyn_resource().Table(table_name).delete_item(Key=delete_keys)
+
+ except ClientError as err:
+ logger.error(
+ "Couldn't delete item %s from table %s. Here's why: %s: %s",
+ id,
+ table_name,
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ @staticmethod
+ def dyn_batch_get(batch_keys: Dict[str, Any]) -> Dict[str, List[Any]]:
+ remaining_tries = 5
+ sleepy_time = 1 # Start with 1 second of sleep, then exponentially increase.
+ retrieved: Dict[str, List[Any]] = {key: [] for key in batch_keys}
+ while batch_keys and remaining_tries:
+ response = DynHelpers.dyn_resource().batch_get_item(RequestItems=batch_keys)
+ # Collect any retrieved items and retry unprocessed keys.
+ for key in response.get("Responses", []):
+ retrieved[key] += response["Responses"][key]
+
+ batch_keys = response["UnprocessedKeys"]
+
+ logger.info(
+ "%s unprocessed keys returned. Sleep, then retry.",
+ len(batch_keys),
+ )
+ remaining_tries -= 1
+ if batch_keys and remaining_tries:
+ logger.info("Sleeping for %s seconds.", sleepy_time)
+ time.sleep(sleepy_time)
+ sleepy_time = min(sleepy_time * 2, 32)
+
+ return retrieved
+
+ @staticmethod
+ def dyn_batch_write(table_name: str, batch_items: List[Dict[str, Any]]) -> None:
+ try:
+ with DynHelpers.dyn_resource().Table(table_name).batch_writer() as batch:
+ for batch_item in batch_items:
+ if batch_item["operation"] == "DELETE":
+ batch.delete_item(Key=batch_item["key"])
+ elif batch_item["operation"] == "PUT":
+ batch.put_item(Item=batch_item["item"])
+
+ except Exception as err:
+ logger.error(msg=f"Error while batch writing: {err}")
+ raise
+
+ @staticmethod
+ def dyn_scan(
+ *args: Any, table: Optional[str] = None, **kwargs: Any
+ ) -> Generator[List[Dict[str, Any]], None, None]:
+ scan_kwargs = {k: v for k, v in kwargs.items() if v}
+
+ logger.info("Running dynamo scan on %s", table, extra={"kwargs": scan_kwargs})
+
+ while scan_kwargs.get("LastEvaluatedKey", "start"):
+ if scan_kwargs.get("LastEvaluatedKey", None):
+ scan_kwargs["ExclusiveStartKey"] = scan_kwargs.pop("LastEvaluatedKey")
+
+ try:
+ response = DynHelpers.dyn_resource().Table(table).scan(**scan_kwargs)
+ logger.info("Scan response %s", table, extra={"response": response})
+ scan_kwargs["LastEvaluatedKey"] = response.get("LastEvaluatedKey")
+
+ yield response.get("Items")
+ except ClientError as err:
+ logger.error(
+ "Couldn't scan %s. Here's why: %s: %s",
+ table,
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ @staticmethod
+ def dyn_query(
+ table_name: str,
+ key_condition_expression: str,
+ selection: str = "ALL_ATTRIBUTES",
+ projection_expression: Optional[str] = None,
+ expression_attribute_names: Optional[Dict[str, str]] = None,
+ expression_attribute_values: Optional[Dict[str, str]] = None,
+ ) -> Any:
+ function_kwargs: Dict[str, Any] = {
+ "KeyConditionExpression": key_condition_expression
+ }
+ try:
+ if projection_expression and selection == "SPECIFIC_ATTRIBUTES":
+ function_kwargs["Select"] = selection
+ elif projection_expression:
+ function_kwargs["ProjectionExpression"] = projection_expression
+ if expression_attribute_names:
+ function_kwargs["ExpressionAttributeNames"] = expression_attribute_names
+ if expression_attribute_values:
+ function_kwargs[
+ "ExpressionAttributeValues"
+ ] = expression_attribute_values
+ response = (
+ DynHelpers.dyn_resource().Table(table_name).query(**function_kwargs)
+ )
+ except ClientError as err:
+ logger.error(
+ "Couldn't query item %s from table %s. Here's why: %s: %s",
+ key_condition_expression,
+ table_name,
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ raise
+
+ return response["Items"]
diff --git a/source/lib/cms_common/boto3_wrappers/tests/__init__.py b/source/lib/cms_common/boto3_wrappers/tests/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/lib/cms_common/boto3_wrappers/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/lib/cms_common/boto3_wrappers/tests/fixture_dynamo_crud.py b/source/lib/cms_common/boto3_wrappers/tests/fixture_dynamo_crud.py
new file mode 100644
index 00000000..26f3a01b
--- /dev/null
+++ b/source/lib/cms_common/boto3_wrappers/tests/fixture_dynamo_crud.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import os
+from typing import Dict, Generator
+from unittest.mock import patch
+
+# Third Party Libraries
+import pytest
+from moto import mock_aws
+
+# AWS Libraries
+import boto3
+
+
+@pytest.fixture(name="mocked_module_env_vars_values", scope="session")
+def fixture_mocked_module_env_vars_values() -> Dict[str, str]:
+ return {
+ "APPLICATION_TYPE": "test-application-type",
+ "SOLUTION_ID": "test-solution-id",
+ "SOLUTION_NAME": "test-solution-name",
+ "SOLUTION_VERSION": "v0.0.0",
+ "S3_ASSET_BUCKET_BASE_NAME": "test-bucket-name",
+ "S3_ASSET_KEY_PREFIX": "test-key-prefix",
+ "USER_AGENT_STRING": "test-user-agent-string",
+ }
+
+
+@pytest.fixture(scope="module", autouse=True)
+def fixture_mock_dynamo_env_vars(
+ mocked_module_env_vars_values: Dict[str, str]
+) -> Generator[None, None, None]:
+ env_vars = os.environ.copy()
+ env_vars.update(mocked_module_env_vars_values)
+ with patch.dict(os.environ, env_vars):
+ yield
+
+
+@pytest.fixture(name="dynamodb_table")
+def fixture_dynamodb_table() -> Generator[str, None, None]:
+ with mock_aws():
+ table_name = "test_table"
+ table = boto3.resource("dynamodb")
+ table.create_table(
+ AttributeDefinitions=[
+ {
+ "AttributeName": "id",
+ "AttributeType": "S",
+ },
+ ],
+ TableName=table_name,
+ KeySchema=[
+ {"AttributeName": "id", "KeyType": "HASH"},
+ ],
+ BillingMode="PAY_PER_REQUEST",
+ )
+ table.Table(table_name).put_item(
+ Item={
+ "id": "test_id_1",
+ "test_val": "test_val_1",
+ }
+ )
+ table.Table(table_name).put_item(
+ Item={
+ "id": "test_id_2",
+ "test_val": "test_val_2",
+ }
+ )
+ yield table_name
diff --git a/source/lib/cms_common/boto3_wrappers/tests/test_dynamo_crud.py b/source/lib/cms_common/boto3_wrappers/tests/test_dynamo_crud.py
new file mode 100644
index 00000000..a4802130
--- /dev/null
+++ b/source/lib/cms_common/boto3_wrappers/tests/test_dynamo_crud.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from typing import Any, Dict
+
+# Third Party Libraries
+import pytest
+from moto import mock_aws
+
+# AWS Libraries
+import boto3
+
+# Connected Mobility Solution on AWS
+from ..dynamo_crud import DynHelpers
+
+
+@mock_aws
+def test_dyn_resource() -> None:
+ dynamo = DynHelpers.dyn_resource()
+ assert dynamo and DynHelpers.dynamo_object
+
+
+def test_get_all(dynamodb_table: str) -> None:
+ items = DynHelpers.get_all(table=dynamodb_table, Limit=1)
+ assert len(items) == 2
+
+
+def test_put_item(dynamodb_table: str) -> None:
+ new_item = {
+ "id": "test_put",
+ "test_val": "test_val_put",
+ }
+ DynHelpers.put_item(dynamodb_table, new_item)
+
+ dynamodb = boto3.resource("dynamodb")
+ item = dynamodb.Table(dynamodb_table).get_item(Key={"id": new_item["id"]})
+ assert item["Item"]
+
+
+def test_get_item(dynamodb_table: str) -> None:
+ response = DynHelpers.get_item(dynamodb_table, {"id": "test_id_1"})
+ assert response
+
+
+def test_update_item(dynamodb_table: str) -> None:
+ item: Dict[str, Any] = {"id": "test_id_1"}
+ updated_test_val = "test_val_1_updated"
+
+ DynHelpers.update_item(
+ dynamodb_table,
+ item,
+ "SET test_val = :updated_test_val",
+ {":updated_test_val": updated_test_val},
+ )
+
+ dynamodb = boto3.resource("dynamodb")
+ item = dynamodb.Table(dynamodb_table).get_item(Key={"id": item["id"]}) # type: ignore[assignment]
+
+ assert item["Item"]["test_val"] == updated_test_val
+
+
+def test_delete_item(dynamodb_table: str) -> None:
+ DynHelpers.get_item(dynamodb_table, {"id": "test_id_1"})
+ DynHelpers.delete_item(dynamodb_table, {"id": "test_id_1"})
+ with pytest.raises(KeyError):
+ DynHelpers.get_item(dynamodb_table, {"id": "test_id_1"})
+
+
+def test_dyn_batch_get(dynamodb_table: str) -> None:
+ keys = ["test_id_1", "test_id_2"]
+ batch_keys = {dynamodb_table: {"Keys": [{"id": key} for key in keys]}}
+ response = DynHelpers.dyn_batch_get(batch_keys)
+ assert len(response[dynamodb_table]) == 2
+
+
+def test_dyn_scan(dynamodb_table: str) -> None:
+ items = DynHelpers.dyn_scan(table=dynamodb_table, Limit=1)
+ assert len(list(items)) == 2
+
+
+def test_dyn_batch_write(dynamodb_table: str) -> None:
+ items = [
+ {
+ "operation": "PUT",
+ "item": {
+ "id": "test_id_3",
+ "test_val": "test_val_3",
+ },
+ },
+ {
+ "operation": "PUT",
+ "item": {
+ "id": "test_id_4",
+ "test_val": "test_val_4",
+ },
+ },
+ ]
+ DynHelpers.dyn_batch_write(dynamodb_table, items)
+ response = DynHelpers.dyn_batch_get(
+ {dynamodb_table: {"Keys": [{"id": key} for key in ["test_id_3", "test_id_4"]]}}
+ )
+ assert len(response[dynamodb_table]) == 2
+ assert response[dynamodb_table][0]["id"] == "test_id_3"
+ assert response[dynamodb_table][1]["id"] == "test_id_4"
+ assert response[dynamodb_table][0]["test_val"] == "test_val_3"
+ assert response[dynamodb_table][1]["test_val"] == "test_val_4"
+
+
+def test_dyn_batch_delete(dynamodb_table: str) -> None:
+ items = [
+ {
+ "operation": "DELETE",
+ "key": {"id": "test_id_3"},
+ },
+ {
+ "operation": "DELETE",
+ "key": {"id": "test_id_4"},
+ },
+ ]
+
+ DynHelpers.dyn_batch_write(dynamodb_table, items)
+ response = DynHelpers.dyn_batch_get(
+ {dynamodb_table: {"Keys": [{"id": key} for key in ["test_id_3", "test_id_4"]]}}
+ )
+ assert len(response[dynamodb_table]) == 0
+
+
+def test_dyn_query(dynamodb_table: str) -> None:
+ response = DynHelpers.dyn_query(
+ table_name=dynamodb_table,
+ key_condition_expression="id=:id",
+ projection_expression="#I, #V",
+ expression_attribute_names={
+ "#I": "id",
+ "#V": "test_val",
+ },
+ expression_attribute_values={":id": "test_id_1"},
+ )
+ assert len(response) == 1
+ assert response[0]["id"] == "test_id_1"
+ assert response[0]["test_val"] == "test_val_1"
diff --git a/source/lib/cms_common/cache/__init__.py b/source/lib/cms_common/cache/__init__.py
new file mode 100644
index 00000000..39ac97bb
--- /dev/null
+++ b/source/lib/cms_common/cache/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from .ttl_cache import get_ttl_cache_check
diff --git a/source/lib/cms_common/cache/tests/__init__.py b/source/lib/cms_common/cache/tests/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/lib/cms_common/cache/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/lib/cms_common/cache/tests/test_ttl_cache.py b/source/lib/cms_common/cache/tests/test_ttl_cache.py
new file mode 100644
index 00000000..d52beea2
--- /dev/null
+++ b/source/lib/cms_common/cache/tests/test_ttl_cache.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from time import sleep
+
+# Connected Mobility Solution on AWS
+from ..ttl_cache import get_ttl_cache_check
+
+
+def test_get_ttl_cache_check_hit() -> None:
+ ttl_cache_check_one = get_ttl_cache_check(ttl_in_seconds=5)
+ ttl_cache_check_two = get_ttl_cache_check(ttl_in_seconds=5)
+ assert ttl_cache_check_two - ttl_cache_check_one == 0
+
+
+def test_get_ttl_cache_check_miss() -> None:
+ ttl_cache_check_one = get_ttl_cache_check(ttl_in_seconds=2)
+ sleep(2)
+ ttl_cache_check_two = get_ttl_cache_check(ttl_in_seconds=2)
+ assert ttl_cache_check_two - ttl_cache_check_one == 1
diff --git a/source/lib/cms_common/cache/ttl_cache.py b/source/lib/cms_common/cache/ttl_cache.py
new file mode 100644
index 00000000..5dd60278
--- /dev/null
+++ b/source/lib/cms_common/cache/ttl_cache.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import time
+
+TEN_MINUTES_IN_SECONDS = 600
+
+
+def get_ttl_cache_check(ttl_in_seconds: int = TEN_MINUTES_IN_SECONDS) -> int:
+ return round(time.time() / ttl_in_seconds)
diff --git a/source/lib/cms_common/config/__init__.py b/source/lib/cms_common/config/__init__.py
new file mode 100644
index 00000000..b61285f3
--- /dev/null
+++ b/source/lib/cms_common/config/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from .metrics import OperationalMetricsInput
+from .resource_names import (
+ ResourceName,
+ ResourcePrefix,
+ get_application_level_path_prefix,
+ remove_leading_slash,
+)
+from .ssm import (
+ get_resolvable_ssm_deployment_uuid,
+ get_resolvable_ssm_metrics_enabled,
+ get_resolvable_ssm_metrics_url,
+ resolve_ssm_parameter,
+)
+from .stack_inputs import (
+ S3AssetConfigInputs,
+ SolutionConfigInputs,
+ create_solution_tags_for_stack,
+ create_stack_description,
+)
diff --git a/source/lib/cms_common/config/metrics.py b/source/lib/cms_common/config/metrics.py
new file mode 100644
index 00000000..720de991
--- /dev/null
+++ b/source/lib/cms_common/config/metrics.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from dataclasses import dataclass
+
+# Connected Mobility Solution on AWS
+from .ssm import (
+ get_resolvable_ssm_deployment_uuid,
+ get_resolvable_ssm_metrics_enabled,
+ get_resolvable_ssm_metrics_url,
+)
+
+
+@dataclass(frozen=True)
+class OperationalMetricsInput:
+ metrics_url: str
+ report_metrics_enabled: str
+ deployment_uuid: str
+
+ @classmethod
+ def from_app_unique_id(cls, app_unique_id: str) -> "OperationalMetricsInput":
+ return OperationalMetricsInput(
+ metrics_url=get_resolvable_ssm_metrics_url(app_unique_id=app_unique_id),
+ report_metrics_enabled=get_resolvable_ssm_metrics_enabled(
+ app_unique_id=app_unique_id
+ ),
+ deployment_uuid=get_resolvable_ssm_deployment_uuid(
+ app_unique_id=app_unique_id
+ ),
+ )
diff --git a/source/lib/cms_common/config/resource_names.py b/source/lib/cms_common/config/resource_names.py
new file mode 100644
index 00000000..f5c66423
--- /dev/null
+++ b/source/lib/cms_common/config/resource_names.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+# Standard Library
+
+# AWS Libraries
+from aws_cdk import Fn
+
+SOLUTIONS_PREFIX = "solution"
+
+# NOTE: These functions must use Cfn functions for string manipulation since AppUniqueId can be a token and therefore not resolvable yet in the template.
+# This is not necessary for basic string concatenation or f strings as Cloud Formation handles this automatically.
+
+
+def get_application_level_path_prefix(
+ app_unique_id: str, leading_slash: bool = False
+) -> str:
+ path_prefix = f"{SOLUTIONS_PREFIX}/{app_unique_id}"
+ return f"/{path_prefix}" if leading_slash else path_prefix
+
+
+def remove_leading_slash(string: str) -> str:
+ return string[1:] if string[0] == "/" else string
+
+
+class ResourcePrefix:
+ @staticmethod
+ def slash_separated(
+ app_unique_id: str, module_name: str, leading_slash: bool = False
+ ) -> str:
+ return f"{get_application_level_path_prefix(app_unique_id=app_unique_id, leading_slash=leading_slash)}/{module_name}"
+
+ @staticmethod
+ def hyphen_separated(app_unique_id: str, module_name: str) -> str:
+ return f"{app_unique_id}-{module_name}"
+
+ @staticmethod
+ def only_underscore_separated(app_unique_id: str, module_name: str) -> str:
+ prefix = f"{app_unique_id}_{module_name}"
+ return Fn.join("_", Fn.split("-", prefix))
+
+
+class ResourceName:
+ @staticmethod
+ def slash_separated(prefix: str, name: str) -> str:
+ return f"{prefix}/{name}"
+
+ @staticmethod
+ def hyphen_separated(prefix: str, name: str) -> str:
+ return f"{prefix}-{name}"
+
+ @staticmethod
+ def underscore_separated(prefix: str, name: str) -> str:
+ return f"{prefix}_{name}"
diff --git a/source/lib/cms_common/config/ssm.py b/source/lib/cms_common/config/ssm.py
new file mode 100644
index 00000000..947178cf
--- /dev/null
+++ b/source/lib/cms_common/config/ssm.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from ..resource_names.module_short_names import CMSModuleShortNames
+from .resource_names import ResourceName, ResourcePrefix
+
+
+def resolve_ssm_parameter(parameter_name: str) -> str:
+ # parameter_name should include any leading slashes that are expected in the ssm parameter name
+ return f"{{{{resolve:ssm:{parameter_name}}}}}"
+
+
+def get_resolvable_ssm_deployment_uuid(app_unique_id: str) -> str:
+ deployment_uuid_ssm_parameter_name = ResourceName.slash_separated(
+ prefix=ResourcePrefix.slash_separated(
+ app_unique_id=app_unique_id,
+ module_name=CMSModuleShortNames.CONFIG,
+ leading_slash=True,
+ ),
+ name="deployment-uuid",
+ )
+ return resolve_ssm_parameter(deployment_uuid_ssm_parameter_name)
+
+
+def get_resolvable_ssm_metrics_url(app_unique_id: str) -> str:
+ metrics_url_ssm_parameter_name = ResourceName.slash_separated(
+ prefix=ResourcePrefix.slash_separated(
+ app_unique_id=app_unique_id,
+ module_name=CMSModuleShortNames.CONFIG,
+ leading_slash=True,
+ ),
+ name="metrics/url",
+ )
+ return resolve_ssm_parameter(metrics_url_ssm_parameter_name)
+
+
+def get_resolvable_ssm_metrics_enabled(app_unique_id: str) -> str:
+ metrics_enabled_ssm_parameter_name = ResourceName.slash_separated(
+ prefix=ResourcePrefix.slash_separated(
+ app_unique_id=app_unique_id,
+ module_name=CMSModuleShortNames.CONFIG,
+ leading_slash=True,
+ ),
+ name="metrics/enabled",
+ )
+ return resolve_ssm_parameter(metrics_enabled_ssm_parameter_name)
diff --git a/source/lib/cms_common/config/stack_inputs.py b/source/lib/cms_common/config/stack_inputs.py
new file mode 100644
index 00000000..910b4b28
--- /dev/null
+++ b/source/lib/cms_common/config/stack_inputs.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from dataclasses import dataclass
+from typing import Optional
+
+# AWS Libraries
+from aws_cdk import App, Tags
+
+
+@dataclass(frozen=True)
+class S3AssetConfigInputs:
+ bucket_base_name: str
+ object_key_prefix: str
+
+
+@dataclass(frozen=True)
+class SolutionConfigInputs:
+ solution_name: str
+ solution_id: str
+ solution_version: str
+ application_type: str
+ module_name: str
+ module_short_name: str
+ capability_id: Optional[str]
+
+ def get_user_agent_string(self) -> str:
+ if self.capability_id is None:
+ return f"AWSSOLUTION/{self.solution_id}/{self.solution_version}"
+
+ return f"AWSSOLUTION/{self.solution_id}/{self.solution_version} AWSSOLUTION-CAPABILITY/{self.capability_id}/{self.solution_version}"
+
+
+def create_stack_description(solution_config: SolutionConfigInputs) -> str:
+ return (
+ f"({solution_config.solution_id}-{solution_config.capability_id}) "
+ f"{solution_config.solution_name} - {solution_config.module_name}. "
+ f"Version {solution_config.solution_version}"
+ )
+
+
+def create_solution_tags_for_stack(
+ app: App, solution_config: SolutionConfigInputs
+) -> None:
+ Tags.of(app).add("Solutions:ModuleName", solution_config.module_name)
+ Tags.of(app).add("Solutions:SolutionName", solution_config.solution_name)
+ Tags.of(app).add("Solutions:SolutionID", solution_config.solution_id)
+ Tags.of(app).add("Solutions:SolutionVersion", solution_config.solution_version)
+ Tags.of(app).add("Solutions:ApplicationType", solution_config.application_type)
diff --git a/source/lib/cms_common/config/tests/__init__.py b/source/lib/cms_common/config/tests/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/lib/cms_common/config/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/lib/cms_common/config/tests/fixture_config.py b/source/lib/cms_common/config/tests/fixture_config.py
new file mode 100644
index 00000000..d5a5d3bc
--- /dev/null
+++ b/source/lib/cms_common/config/tests/fixture_config.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Third Party Libraries
+import pytest
+
+# Connected Mobility Solution on AWS
+from ..stack_inputs import SolutionConfigInputs
+
+
+@pytest.fixture(name="solution_config")
+def fixture_solution_config() -> SolutionConfigInputs:
+ return SolutionConfigInputs(
+ solution_id="test-solution-id",
+ solution_name="test-solution-name",
+ solution_version="test-solution-version",
+ module_name="test-module-name",
+ module_short_name="test-module-short-name",
+ capability_id="test-capability-id",
+ application_type="test-application-type",
+ )
diff --git a/source/lib/cms_common/config/tests/test_resource_names.py b/source/lib/cms_common/config/tests/test_resource_names.py
new file mode 100644
index 00000000..ce46e61d
--- /dev/null
+++ b/source/lib/cms_common/config/tests/test_resource_names.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from ..resource_names import (
+ SOLUTIONS_PREFIX,
+ ResourceName,
+ ResourcePrefix,
+ get_application_level_path_prefix,
+ remove_leading_slash,
+)
+
+
+def test_get_application_level_path_prefix_no_leading_slash() -> None:
+ assert (
+ get_application_level_path_prefix(app_unique_id="test-id") == "solution/test-id"
+ )
+
+
+def test_get_application_level_path_prefix_leading_slash() -> None:
+ assert (
+ get_application_level_path_prefix(app_unique_id="test-id", leading_slash=True)
+ == "/solution/test-id"
+ )
+
+
+def test_remove_leading_slash_with_leading_slash_argument() -> None:
+ assert remove_leading_slash("/with/leading/slash") == "with/leading/slash"
+
+
+def test_remove_leading_slash_without_leading_slash_argument() -> None:
+ assert remove_leading_slash("without/leading/slash") == "without/leading/slash"
+
+
+def test_resource_prefix_slash_separated_leading_slash() -> None:
+ assert (
+ ResourcePrefix.slash_separated(
+ app_unique_id="test-uid", module_name="test-module-name", leading_slash=True
+ )
+ == f"/{SOLUTIONS_PREFIX}/test-uid/test-module-name"
+ )
+
+
+def test_resource_prefix_slash_separated_no_leading_slash() -> None:
+ assert (
+ ResourcePrefix.slash_separated(
+ app_unique_id="test-uid", module_name="test-module-name"
+ )
+ == f"{SOLUTIONS_PREFIX}/test-uid/test-module-name"
+ )
+
+
+def test_resource_prefix_hyphen_separated() -> None:
+ assert (
+ ResourcePrefix.hyphen_separated(
+ app_unique_id="test-uid", module_name="test-module-name"
+ )
+ == "test-uid-test-module-name"
+ )
+
+
+def test_resource_name_slash_separated() -> None:
+ assert (
+ ResourceName.slash_separated(prefix="test-prefix", name="test-name")
+ == "test-prefix/test-name"
+ )
+
+
+def test_resource_name_hyphen_separated() -> None:
+ assert (
+ ResourceName.hyphen_separated(prefix="test-prefix", name="test-name")
+ == "test-prefix-test-name"
+ )
+
+
+def test_resource_name_underscore_separated() -> None:
+ assert (
+ ResourceName.underscore_separated(prefix="test-prefix", name="test-name")
+ == "test-prefix_test-name"
+ )
diff --git a/source/lib/cms_common/config/tests/test_stack_inputs.py b/source/lib/cms_common/config/tests/test_stack_inputs.py
new file mode 100644
index 00000000..e9dbef93
--- /dev/null
+++ b/source/lib/cms_common/config/tests/test_stack_inputs.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from ..stack_inputs import SolutionConfigInputs, create_stack_description
+
+
+def test_create_stack_description(solution_config: SolutionConfigInputs) -> None:
+ assert create_stack_description(solution_config=solution_config) == (
+ f"({solution_config.solution_id}-{solution_config.capability_id}) "
+ f"{solution_config.solution_name} - {solution_config.module_name}. "
+ f"Version {solution_config.solution_version}"
+ )
+
+
+def test_user_agent_string(solution_config: SolutionConfigInputs) -> None:
+ assert solution_config.get_user_agent_string() == (
+ f"AWSSOLUTION/{solution_config.solution_id}/{solution_config.solution_version} AWSSOLUTION-CAPABILITY/{solution_config.capability_id}/{solution_config.solution_version}"
+ )
diff --git a/source/lib/cms_common/constructs/__init__.py b/source/lib/cms_common/constructs/__init__.py
new file mode 100644
index 00000000..4df0ada8
--- /dev/null
+++ b/source/lib/cms_common/constructs/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from .app_registry import AppRegistryConstruct, AppRegistryInputs
+from .app_unique_id import AppUniqueId
+from .cdk_lambda_vpc_config_construct import CDKLambdasVpcConfigConstruct
+from .custom_resource_lambda import CustomResourceLambdaConstruct
+from .identity_provider_config import IdentityProviderConfig
+from .lambda_dependencies import LambdaDependenciesConstruct
+from .vpc_construct import (
+ UnsafeDynamicVpc,
+ VpcConfig,
+ VpcConstruct,
+ create_vpc_config,
+ get_vpc_name,
+)
diff --git a/source/lib/cms_common/constructs/app_registry.py b/source/lib/cms_common/constructs/app_registry.py
new file mode 100644
index 00000000..2d75ac51
--- /dev/null
+++ b/source/lib/cms_common/constructs/app_registry.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from dataclasses import dataclass
+
+# AWS Libraries
+from aws_cdk import Stack, aws_servicecatalogappregistry
+from constructs import Construct
+
+
+@dataclass(frozen=True)
+class AppRegistryInputs:
+ application_name: str
+ application_type: str
+ solution_id: str
+ solution_name: str
+ solution_version: str
+
+
+class AppRegistryConstruct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ app_registry_inputs: AppRegistryInputs,
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ region = Stack.of(self).region
+ account = Stack.of(self).account
+
+ cfn_application = aws_servicecatalogappregistry.CfnApplication(
+ self,
+ "app-registry-application",
+ name=f"{app_registry_inputs.application_name}-{region}-{account}",
+ )
+
+ attribute_group = aws_servicecatalogappregistry.CfnAttributeGroup(
+ self,
+ "default-application-attributes",
+ name=f"{app_registry_inputs.application_name}-{region}-{account}",
+ description="Attribute group for solution information",
+ attributes={
+ "ApplicationType": app_registry_inputs.application_type,
+ "Version": app_registry_inputs.solution_version,
+ "SolutionID": app_registry_inputs.solution_id,
+ "SolutionName": app_registry_inputs.solution_name,
+ },
+ )
+
+ # Associate attribute group with registry
+ aws_servicecatalogappregistry.CfnAttributeGroupAssociation(
+ self,
+ "app-registry-application-attribute-association",
+ application=cfn_application.attr_id,
+ attribute_group=attribute_group.attr_id,
+ )
+
+ # Associate stacks with application registry, including this stack.
+ for child in Stack.of(self).node.find_all():
+ if Stack.is_stack(child):
+ stack = Stack.of(child)
+ aws_servicecatalogappregistry.CfnResourceAssociation(
+ stack,
+ "app-registry-application-stack-association",
+ application=cfn_application.attr_id,
+ resource=stack.stack_id,
+ resource_type="CFN_STACK",
+ )
diff --git a/source/lib/cms_common/constructs/app_unique_id.py b/source/lib/cms_common/constructs/app_unique_id.py
new file mode 100644
index 00000000..e57ac947
--- /dev/null
+++ b/source/lib/cms_common/constructs/app_unique_id.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# AWS Libraries
+from aws_cdk import CfnParameter, aws_ssm
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..config.resource_names import ResourcePrefix, get_application_level_path_prefix
+from ..config.ssm import resolve_ssm_parameter
+
+
+class AppUniqueId:
+ @staticmethod
+ def create_cfn_parameter(
+ scope: Construct,
+ ) -> str:
+ app_unique_id = CfnParameter(
+ scope,
+ "AppUniqueId",
+ type="String",
+ description="Application unique identifier used to uniquely name resources within the stack.",
+ allowed_pattern=r"^(?!-)[a-z0-9-]+(? aws_ssm.StringParameter:
+ return aws_ssm.StringParameter(
+ scope,
+ "ssm-app-unique-id",
+ parameter_name=f"/{get_application_level_path_prefix(app_unique_id)}",
+ string_value=app_unique_id,
+ description="SSM parameter to register an app unique ID.",
+ simple_name=True,
+ )
+
+ @staticmethod
+ def register_module(
+ scope: Construct, app_unique_id: str, module_name: str
+ ) -> aws_ssm.StringParameter:
+ return aws_ssm.StringParameter(
+ scope,
+ "ssm-app-unique-id-register-module",
+ parameter_name=ResourcePrefix.slash_separated(
+ app_unique_id=app_unique_id,
+ module_name=module_name,
+ leading_slash=True,
+ ),
+ string_value=resolve_ssm_parameter(
+ parameter_name=get_application_level_path_prefix(
+ app_unique_id, leading_slash=True
+ )
+ ),
+ description="SSM parameter to register a module with an app unique ID.",
+ simple_name=True,
+ )
diff --git a/source/lib/cms_common/constructs/cdk_lambda_vpc_config_construct.py b/source/lib/cms_common/constructs/cdk_lambda_vpc_config_construct.py
new file mode 100644
index 00000000..5edd61de
--- /dev/null
+++ b/source/lib/cms_common/constructs/cdk_lambda_vpc_config_construct.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from typing import List
+
+# AWS Libraries
+from aws_cdk import Stack, aws_ec2
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..aspects.nag_suppression import NagSuppression, NagType
+from ..policy_generators.ec2_vpc import generate_ec2_vpc_policy
+from .vpc_construct import VpcConstruct
+
+
+class CDKLambdasVpcConfigConstruct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ vpc_construct: VpcConstruct,
+ subnets: List[str],
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ base_security_group = aws_ec2.SecurityGroup(
+ self, "security-group", allow_all_outbound=True, vpc=vpc_construct.vpc # type: ignore[arg-type] # NOSONAR
+ )
+
+ self.security_groups = [
+ Stack.of(self).get_logical_id(base_security_group.node.default_child) # type: ignore[arg-type]
+ ]
+
+ self.subnets = subnets
+ self.ec2_vpc_policy_document = generate_ec2_vpc_policy(
+ self,
+ vpc_construct=vpc_construct,
+ subnet_selection=vpc_construct.private_subnet_selection,
+ authorized_service="lambda.amazonaws.com",
+ )
+
+ NagSuppression.add_inline_suppression(
+ node=base_security_group.node.default_child,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "W5",
+ "reason": "Unable to know egress requirement. leaving open for now",
+ },
+ {
+ "id": "W40",
+ "reason": "Unable to know egress requirement. leaving open for now",
+ },
+ ]
+ },
+ nag_type=NagType.CFN_NAG,
+ )
diff --git a/source/lib/cms_common/constructs/custom_resource_lambda.py b/source/lib/cms_common/constructs/custom_resource_lambda.py
new file mode 100644
index 00000000..fc0eefee
--- /dev/null
+++ b/source/lib/cms_common/constructs/custom_resource_lambda.py
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# AWS Libraries
+from aws_cdk import Duration, aws_ec2, aws_iam, aws_lambda, aws_logs
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..aspects.nag_suppression import NagSuppression, NagType
+from ..policy_generators.cloudwatch import (
+ generate_lambda_cloudwatch_logs_policy_document,
+)
+from ..policy_generators.ec2_vpc import generate_ec2_vpc_policy
+from .vpc_construct import VpcConstruct
+
+
+class CustomResourceLambdaConstruct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ dependency_layer: aws_lambda.LayerVersion,
+ unique_id: str,
+ name: str,
+ user_agent_string: str,
+ vpc_construct: VpcConstruct,
+ asset_path: str,
+ suffix: str = "custom-resource",
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ custom_resource_lambda_name = f"{unique_id}-{name}-{suffix}"
+
+ self.role = aws_iam.Role(
+ self,
+ "lambda-role",
+ assumed_by=aws_iam.ServicePrincipal("lambda.amazonaws.com"),
+ inline_policies={
+ "lambda-logs-policy": generate_lambda_cloudwatch_logs_policy_document(
+ self, custom_resource_lambda_name
+ ),
+ "ec2-policy": generate_ec2_vpc_policy(
+ self,
+ vpc_construct=vpc_construct,
+ subnet_selection=vpc_construct.private_subnet_selection,
+ authorized_service="lambda.amazonaws.com",
+ ),
+ },
+ )
+
+ NagSuppression.add_inline_suppression(
+ node=self.role.node.default_child,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "reason": "Wildcard permissions required to write to log streams.",
+ },
+ {
+ "id": "W11",
+ "reason": "ec2 Network Interfaces permissions need to be wildcard",
+ },
+ ]
+ },
+ nag_type=NagType.CFN_NAG,
+ )
+
+ # Can't include unique id in the nag suppression since it is typically a Cfn ref. Wildcard it instead.
+ NagSuppression.add_inline_suppression(
+ node=self.role.node.default_child,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "appliesTo": [
+ f"Resource::arn::logs:::log-group:/aws/lambda/-{name}-{suffix}:log-stream:*",
+ f"Resource::arn::logs:::log-group:/aws/lambda/-{name}-{suffix}:log-stream:*",
+ ],
+ "reason": "Log retention lambda uses policies that require wildcard permissions",
+ },
+ {
+ "id": "AwsSolutions-IAM5",
+ "appliesTo": [
+ "Resource::arn::ec2:::network-interface/*",
+ "Resource::*",
+ ],
+ "reason": "ec2 Network Interfaces permissions need to be wildcard",
+ },
+ ]
+ },
+ nag_type=NagType.CDK_NAG,
+ )
+
+ self.security_group = aws_ec2.SecurityGroup(
+ self,
+ "security-group",
+ vpc=vpc_construct.vpc, # type: ignore[arg-type]
+ allow_all_outbound=True, # NOSONAR
+ )
+
+ NagSuppression.add_inline_suppression(
+ node=self.security_group.node.default_child,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "W40",
+ "reason": "Lambdas need outbound communication to contact other resources in VPC",
+ },
+ {
+ "id": "W5",
+ "reason": "Lambdas are inside Private Subnets and may need to communicate to services over internet. So the CIDR is wide open on egress for now",
+ },
+ ]
+ },
+ nag_type=NagType.CFN_NAG,
+ )
+
+ self.function = aws_lambda.Function(
+ self,
+ "lambda-function",
+ code=aws_lambda.Code.from_asset(
+ asset_path,
+ exclude=["**/tests/*"],
+ ),
+ handler="function.main.handler",
+ function_name=custom_resource_lambda_name,
+ role=self.role,
+ runtime=aws_lambda.Runtime.PYTHON_3_10,
+ timeout=Duration.minutes(5),
+ layers=[dependency_layer],
+ memory_size=1024,
+ environment={
+ "USER_AGENT_STRING": user_agent_string,
+ },
+ log_retention=aws_logs.RetentionDays.THREE_MONTHS,
+ vpc=vpc_construct.vpc, # type: ignore[arg-type]
+ vpc_subnets=vpc_construct.private_subnet_selection,
+ security_groups=[self.security_group],
+ )
+ NagSuppression.add_inline_suppression(
+ node=self.function.node.default_child,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-L1",
+ "reason": (
+ "Some libraries used throughout the solution are not yet "
+ "supported in Python 3.11. For consistency, all lambdas are currently "
+ "kept at Python 3.10. Future refactoring of unsupported libraries will "
+ "enable the use of 3.11 throughout the solution."
+ ),
+ }
+ ]
+ },
+ nag_type=NagType.CDK_NAG,
+ )
+
+ NagSuppression.add_inline_suppression(
+ node=self.function.node.default_child,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "W92",
+ "reason": "Ignore reserved concurrent execution requirements for Lambda functions for now.",
+ },
+ ]
+ },
+ nag_type=NagType.CFN_NAG,
+ )
+
+ def add_policy_to_custom_resource_lambda(self, policy: aws_iam.Policy) -> None:
+ self.role.attach_inline_policy(policy)
diff --git a/source/lib/cms_common/constructs/identity_provider_config.py b/source/lib/cms_common/constructs/identity_provider_config.py
new file mode 100644
index 00000000..4cd6b982
--- /dev/null
+++ b/source/lib/cms_common/constructs/identity_provider_config.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# AWS Libraries
+from aws_cdk import CfnParameter, CustomResource
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..config.ssm import resolve_ssm_parameter
+from ..enums.aws_resource_lookup import AwsResourceLookupCustomResourceType
+from ..resource_names.config import ConfigResourceNames
+
+
+class IdentityProviderConfig:
+ @staticmethod
+ def create_cfn_parameter(
+ scope: Construct,
+ ) -> str:
+ identity_provider_id = CfnParameter(
+ scope,
+ "IdentityProviderId",
+ type="String",
+ description="The ID associated with the identity provider configurations used for validation and exchange.",
+ min_length=3,
+ constraint_description=(
+ "The identity provider ID must be a minimum of 3 characters."
+ ),
+ default="cms",
+ ).value_as_string
+
+ return identity_provider_id
+
+ @staticmethod
+ def get_identity_provider_id(scope: Construct, app_unique_id: str) -> str:
+ config_resource_names = ConfigResourceNames.from_app_unique_id(app_unique_id)
+
+ aws_resource_lookup_lambda_arn = resolve_ssm_parameter(
+ parameter_name=config_resource_names.aws_resource_lookup_lambda_arn_ssm_parameter
+ )
+
+ identity_provider_id_custom_resource = CustomResource(
+ scope,
+ "identity-provider-id-custom-resource",
+ service_token=aws_resource_lookup_lambda_arn,
+ resource_type=f"Custom::{AwsResourceLookupCustomResourceType.SSM_PARAMETERS.value}",
+ properties={
+ "Resource": AwsResourceLookupCustomResourceType.SSM_PARAMETERS.value,
+ "ParameterName": config_resource_names.identity_provider_id_ssm_parameter,
+ },
+ )
+
+ return identity_provider_id_custom_resource.get_att_string("parameter_value")
diff --git a/source/lib/cms_common/constructs/lambda_dependencies.py b/source/lib/cms_common/constructs/lambda_dependencies.py
new file mode 100644
index 00000000..9782912e
--- /dev/null
+++ b/source/lib/cms_common/constructs/lambda_dependencies.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import os
+import pathlib
+from io import TextIOWrapper
+from typing import Any
+
+# Third Party Libraries
+import toml
+
+# AWS Libraries
+from aws_cdk import aws_lambda
+from constructs import Construct
+
+
+class LambdaDependencyError(Exception):
+ def __init__(
+ self,
+ message: str = "Failed to install lambda dependencies while building lambda layer.",
+ code: int = 500,
+ ):
+ self.message = message
+ self.code = code
+
+
+class LambdaDependenciesConstruct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ pipfile_path: str,
+ dependency_layer_path: str,
+ **kwargs: Any,
+ ) -> None:
+ super().__init__(scope, construct_id, **kwargs)
+
+ pip_path = f"{dependency_layer_path}/python"
+
+ # Create the directories required for the dependency layer
+ pathlib.Path(pip_path).mkdir(parents=True, exist_ok=True)
+ requirements = f"{dependency_layer_path}/requirements.txt"
+
+ # Copy Pipfile to build directory as requirements.txt format and excluding the large packages
+ with open(pipfile_path, "r", encoding="utf-8") as pipfile:
+ new_pipfile = toml.load(pipfile)
+ with open(requirements, "w", encoding="utf-8") as requirements_file:
+
+ for package, constraint in new_pipfile["packages"].items():
+ if package not in ["boto3", "aws-cdk-lib"]:
+ self.req_formatter(
+ package=package,
+ constraint=constraint,
+ requirements_file=requirements_file,
+ )
+
+ # Install the requirements in the build directory (CDK will use this whole folder to build the zip)
+ requirements_building_exit_code = os.system( # nosec
+ (
+ f"/bin/bash -c 'python -m pip install -q "
+ f"--platform manylinux2014_x86_64 --python-version 3.10 --implementation cp --only-binary=:all: --upgrade --no-cache-dir "
+ f"--target {pip_path} --requirement {requirements}'"
+ )
+ )
+
+ if requirements_building_exit_code > 0:
+ raise LambdaDependencyError("Failed to install lambda layer dependencies.")
+
+ self.dependency_layer = aws_lambda.LayerVersion(
+ self,
+ "lambda-dependency-layer-version",
+ code=aws_lambda.Code.from_asset(dependency_layer_path),
+ compatible_architectures=[
+ aws_lambda.Architecture.X86_64,
+ aws_lambda.Architecture.ARM_64,
+ ],
+ compatible_runtimes=[
+ aws_lambda.Runtime.PYTHON_3_8,
+ aws_lambda.Runtime.PYTHON_3_9,
+ aws_lambda.Runtime.PYTHON_3_10,
+ ],
+ )
+
+ def req_formatter(
+ self, package: str, constraint: Any, requirements_file: TextIOWrapper
+ ) -> None:
+ if constraint == "*":
+ requirements_file.write(package + "\n")
+ else:
+ try:
+ extras = (
+ str(constraint.get("extras", "all"))
+ .replace("'", "")
+ .replace('"', "")
+ )
+
+ # Requirements.txt wildcards are done by not specifying a version, replace with empty string instead
+ version = constraint["version"] if constraint["version"] != "*" else ""
+
+ requirements_file.write(f"{package}{extras} {version}\n")
+ except (TypeError, KeyError, AttributeError):
+ if isinstance(constraint, str):
+ requirements_file.write(f"{package} {constraint}\n")
+
+ if isinstance(constraint, dict) and constraint.get("path"):
+ requirements_file.write(f"{constraint['path']}\n")
diff --git a/source/lib/cms_common/constructs/tests/__init__.py b/source/lib/cms_common/constructs/tests/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/lib/cms_common/constructs/tests/__snapshots__/test_app_registry/test_app_registry_snapshot.json b/source/lib/cms_common/constructs/tests/__snapshots__/test_app_registry/test_app_registry_snapshot.json
new file mode 100644
index 00000000..ebb64274
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/__snapshots__/test_app_registry/test_app_registry_snapshot.json
@@ -0,0 +1,117 @@
+{
+ "Parameters": {
+ "BootstrapVersion": {
+ "Default": "/cdk-bootstrap/hnb659fds/version",
+ "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
+ "Type": "AWS::SSM::Parameter::Value"
+ }
+ },
+ "Resources": {
+ "appregistryapplicationstackassociation": {
+ "Properties": {
+ "Application": {
+ "Fn::GetAtt": [
+ "testappregistryappregistryapplication2A74C8E2",
+ "Id"
+ ]
+ },
+ "Resource": {
+ "Ref": "AWS::StackId"
+ },
+ "ResourceType": "CFN_STACK"
+ },
+ "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation"
+ },
+ "testappregistryappregistryapplication2A74C8E2": {
+ "Properties": {
+ "Name": {
+ "Fn::Join": [
+ "",
+ [
+ "test-application-name-",
+ {
+ "Ref": "AWS::Region"
+ },
+ "-",
+ {
+ "Ref": "AWS::AccountId"
+ }
+ ]
+ ]
+ }
+ },
+ "Type": "AWS::ServiceCatalogAppRegistry::Application"
+ },
+ "testappregistryappregistryapplicationattributeassociation47DF0144": {
+ "Properties": {
+ "Application": {
+ "Fn::GetAtt": [
+ "testappregistryappregistryapplication2A74C8E2",
+ "Id"
+ ]
+ },
+ "AttributeGroup": {
+ "Fn::GetAtt": [
+ "testappregistrydefaultapplicationattributesF88569DD",
+ "Id"
+ ]
+ }
+ },
+ "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation"
+ },
+ "testappregistrydefaultapplicationattributesF88569DD": {
+ "Properties": {
+ "Attributes": {
+ "ApplicationType": "test-application-type",
+ "SolutionID": "test-solution-id",
+ "SolutionName": "test-solution-name",
+ "Version": "test-solution-version"
+ },
+ "Description": "Attribute group for solution information",
+ "Name": {
+ "Fn::Join": [
+ "",
+ [
+ "test-application-name-",
+ {
+ "Ref": "AWS::Region"
+ },
+ "-",
+ {
+ "Ref": "AWS::AccountId"
+ }
+ ]
+ ]
+ }
+ },
+ "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroup"
+ }
+ },
+ "Rules": {
+ "CheckBootstrapVersion": {
+ "Assertions": [
+ {
+ "Assert": {
+ "Fn::Not": [
+ {
+ "Fn::Contains": [
+ [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5"
+ ],
+ {
+ "Ref": "BootstrapVersion"
+ }
+ ]
+ }
+ ]
+ },
+ "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
+ }
+ ]
+ }
+ }
+}
diff --git a/source/lib/cms_common/constructs/tests/__snapshots__/test_app_unique_id/test_app_unique_id_snapshot.json b/source/lib/cms_common/constructs/tests/__snapshots__/test_app_unique_id/test_app_unique_id_snapshot.json
new file mode 100644
index 00000000..48a5dbbc
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/__snapshots__/test_app_unique_id/test_app_unique_id_snapshot.json
@@ -0,0 +1,44 @@
+{
+ "Parameters": {
+ "AppUniqueId": {
+ "AllowedPattern": "^(?!-)[a-z0-9-]+(?"
+ }
+ },
+ "Rules": {
+ "CheckBootstrapVersion": {
+ "Assertions": [
+ {
+ "Assert": {
+ "Fn::Not": [
+ {
+ "Fn::Contains": [
+ [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5"
+ ],
+ {
+ "Ref": "BootstrapVersion"
+ }
+ ]
+ }
+ ]
+ },
+ "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
+ }
+ ]
+ }
+ }
+}
diff --git a/source/lib/cms_common/constructs/tests/__snapshots__/test_cdk_lambda_vpc_config_construct/test_cdk_lambda_vpc_config_construct.json b/source/lib/cms_common/constructs/tests/__snapshots__/test_cdk_lambda_vpc_config_construct/test_cdk_lambda_vpc_config_construct.json
new file mode 100644
index 00000000..8c086267
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/__snapshots__/test_cdk_lambda_vpc_config_construct/test_cdk_lambda_vpc_config_construct.json
@@ -0,0 +1,66 @@
+{
+ "Parameters": {
+ "BootstrapVersion": {
+ "Default": "/cdk-bootstrap/hnb659fds/version",
+ "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
+ "Type": "AWS::SSM::Parameter::Value"
+ }
+ },
+ "Resources": {
+ "testcdklambdavpcconfigconstructlambdasecuritygroup9BD08407": {
+ "Metadata": {
+ "cfn_nag": {
+ "rules_to_suppress": [
+ {
+ "id": "W5",
+ "reason": "Unable to know egress requirement. leaving open for now"
+ },
+ {
+ "id": "W40",
+ "reason": "Unable to know egress requirement. leaving open for now"
+ }
+ ]
+ }
+ },
+ "Properties": {
+ "GroupDescription": "Default/test-cdk-lambda-vpc-config-construct-lambda/security-group",
+ "SecurityGroupEgress": [
+ {
+ "CidrIp": "0.0.0.0/0",
+ "Description": "Allow all outbound traffic by default",
+ "IpProtocol": "-1"
+ }
+ ],
+ "VpcId": "test-vpc-id"
+ },
+ "Type": "AWS::EC2::SecurityGroup"
+ }
+ },
+ "Rules": {
+ "CheckBootstrapVersion": {
+ "Assertions": [
+ {
+ "Assert": {
+ "Fn::Not": [
+ {
+ "Fn::Contains": [
+ [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5"
+ ],
+ {
+ "Ref": "BootstrapVersion"
+ }
+ ]
+ }
+ ]
+ },
+ "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
+ }
+ ]
+ }
+ }
+}
diff --git a/source/lib/cms_common/constructs/tests/__snapshots__/test_custom_resource_lambda/test_custom_resource_lambda_snapshot.json b/source/lib/cms_common/constructs/tests/__snapshots__/test_custom_resource_lambda/test_custom_resource_lambda_snapshot.json
new file mode 100644
index 00000000..d27d39dd
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/__snapshots__/test_custom_resource_lambda/test_custom_resource_lambda_snapshot.json
@@ -0,0 +1,454 @@
+{
+ "Parameters": {
+ "BootstrapVersion": {
+ "Default": "/cdk-bootstrap/hnb659fds/version",
+ "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
+ "Type": "AWS::SSM::Parameter::Value"
+ }
+ },
+ "Resources": {
+ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": {
+ "DependsOn": [
+ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB",
+ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB"
+ ],
+ "Properties": {
+ "Code": {
+ "S3Bucket": {
+ "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}"
+ },
+ "S3Key": "test"
+ },
+ "Handler": "index.handler",
+ "Role": {
+ "Fn::GetAtt": [
+ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB",
+ "Arn"
+ ]
+ },
+ "Runtime": "nodejs18.x",
+ "Timeout": 900
+ },
+ "Type": "AWS::Lambda::Function"
+ },
+ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": {
+ "Properties": {
+ "AssumeRolePolicyDocument": {
+ "Statement": [
+ {
+ "Action": "sts:AssumeRole",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "lambda.amazonaws.com"
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "ManagedPolicyArns": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+ ]
+ ]
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Role"
+ },
+ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB": {
+ "Properties": {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "logs:PutRetentionPolicy",
+ "logs:DeleteRetentionPolicy"
+ ],
+ "Effect": "Allow",
+ "Resource": "*"
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB",
+ "Roles": [
+ {
+ "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB"
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Policy"
+ },
+ "testcustomresourcelambdalambdafunctionC2803C89": {
+ "DependsOn": [
+ "testcustomresourcelambdalambdarole3EB2AE8D"
+ ],
+ "Metadata": {
+ "cdk_nag": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-L1",
+ "reason": "Some libraries used throughout the solution are not yet supported in Python 3.11. For consistency, all lambdas are currently kept at Python 3.10. Future refactoring of unsupported libraries will enable the use of 3.11 throughout the solution."
+ }
+ ]
+ },
+ "cfn_nag": {
+ "rules_to_suppress": [
+ {
+ "id": "W92",
+ "reason": "Ignore reserved concurrent execution requirements for Lambda functions for now."
+ }
+ ]
+ }
+ },
+ "Properties": {
+ "Code": {
+ "S3Bucket": {
+ "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}"
+ },
+ "S3Key": "test"
+ },
+ "Environment": {
+ "Variables": {
+ "USER_AGENT_STRING": "test-user-agent-string"
+ }
+ },
+ "FunctionName": "test-id-test-module-name-custom-resource",
+ "Handler": "function.main.handler",
+ "Layers": [
+ {
+ "Ref": "testlambdadependencieslambdadependencylayerversionAA06C21D"
+ }
+ ],
+ "MemorySize": 1024,
+ "Role": {
+ "Fn::GetAtt": [
+ "testcustomresourcelambdalambdarole3EB2AE8D",
+ "Arn"
+ ]
+ },
+ "Runtime": "python3.10",
+ "Timeout": 300,
+ "VpcConfig": {
+ "SecurityGroupIds": [
+ {
+ "Fn::GetAtt": [
+ "testcustomresourcelambdasecuritygroup8EE64646",
+ "GroupId"
+ ]
+ }
+ ],
+ "SubnetIds": [
+ "test-vpc-private-subnet-1",
+ "test-vpc-private-subnet-2"
+ ]
+ }
+ },
+ "Type": "AWS::Lambda::Function"
+ },
+ "testcustomresourcelambdalambdafunctionLogRetentionD9120E69": {
+ "Properties": {
+ "LogGroupName": {
+ "Fn::Join": [
+ "",
+ [
+ "/aws/lambda/",
+ {
+ "Ref": "testcustomresourcelambdalambdafunctionC2803C89"
+ }
+ ]
+ ]
+ },
+ "RetentionInDays": 90,
+ "ServiceToken": {
+ "Fn::GetAtt": [
+ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A",
+ "Arn"
+ ]
+ }
+ },
+ "Type": "Custom::LogRetention"
+ },
+ "testcustomresourcelambdalambdarole3EB2AE8D": {
+ "Metadata": {
+ "cdk_nag": {
+ "rules_to_suppress": [
+ {
+ "appliesTo": [
+ "Resource::arn::logs:::log-group:/aws/lambda/-test-module-name-custom-resource:log-stream:*",
+ "Resource::arn::logs:::log-group:/aws/lambda/-test-module-name-custom-resource:log-stream:*"
+ ],
+ "id": "AwsSolutions-IAM5",
+ "reason": "Log retention lambda uses policies that require wildcard permissions"
+ },
+ {
+ "appliesTo": [
+ "Resource::arn::ec2:::network-interface/*",
+ "Resource::*"
+ ],
+ "id": "AwsSolutions-IAM5",
+ "reason": "ec2 Network Interfaces permissions need to be wildcard"
+ }
+ ]
+ },
+ "cfn_nag": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "reason": "Wildcard permissions required to write to log streams."
+ },
+ {
+ "id": "W11",
+ "reason": "ec2 Network Interfaces permissions need to be wildcard"
+ }
+ ]
+ }
+ },
+ "Properties": {
+ "AssumeRolePolicyDocument": {
+ "Statement": [
+ {
+ "Action": "sts:AssumeRole",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "lambda.amazonaws.com"
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "Policies": [
+ {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "logs:CreateLogGroup",
+ "logs:CreateLogStream",
+ "logs:PutLogEvents"
+ ],
+ "Effect": "Allow",
+ "Resource": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":logs:",
+ {
+ "Ref": "AWS::Region"
+ },
+ ":",
+ {
+ "Ref": "AWS::AccountId"
+ },
+ ":log-group:/aws/lambda/test-id-test-module-name-custom-resource"
+ ]
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":logs:",
+ {
+ "Ref": "AWS::Region"
+ },
+ ":",
+ {
+ "Ref": "AWS::AccountId"
+ },
+ ":log-group:/aws/lambda/test-id-test-module-name-custom-resource:log-stream:*"
+ ]
+ ]
+ }
+ ]
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "lambda-logs-policy"
+ },
+ {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": "ec2:CreateNetworkInterfacePermission",
+ "Condition": {
+ "StringEquals": {
+ "ec2:AuthorizedService": "lambda.amazonaws.com",
+ "ec2:Subnet": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ec2:",
+ {
+ "Ref": "AWS::Region"
+ },
+ ":",
+ {
+ "Ref": "AWS::AccountId"
+ },
+ ":subnet/test-vpc-private-subnet-1"
+ ]
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ec2:",
+ {
+ "Ref": "AWS::Region"
+ },
+ ":",
+ {
+ "Ref": "AWS::AccountId"
+ },
+ ":subnet/test-vpc-private-subnet-2"
+ ]
+ ]
+ }
+ ]
+ }
+ },
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ec2:",
+ {
+ "Ref": "AWS::Region"
+ },
+ ":",
+ {
+ "Ref": "AWS::AccountId"
+ },
+ ":network-interface/*"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ec2:DescribeNetworkInterfaces",
+ "ec2:CreateNetworkInterface",
+ "ec2:DeleteNetworkInterface"
+ ],
+ "Effect": "Allow",
+ "Resource": "*"
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "ec2-policy"
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Role"
+ },
+ "testcustomresourcelambdasecuritygroup8EE64646": {
+ "Metadata": {
+ "cfn_nag": {
+ "rules_to_suppress": [
+ {
+ "id": "W40",
+ "reason": "Lambdas need outbound communication to contact other resources in VPC"
+ },
+ {
+ "id": "W5",
+ "reason": "Lambdas are inside Private Subnets and may need to communicate to services over internet. So the CIDR is wide open on egress for now"
+ }
+ ]
+ }
+ },
+ "Properties": {
+ "GroupDescription": "Default/test-custom-resource-lambda/security-group",
+ "SecurityGroupEgress": [
+ {
+ "CidrIp": "0.0.0.0/0",
+ "Description": "Allow all outbound traffic by default",
+ "IpProtocol": "-1"
+ }
+ ],
+ "VpcId": "test-vpc-id"
+ },
+ "Type": "AWS::EC2::SecurityGroup"
+ },
+ "testlambdadependencieslambdadependencylayerversionAA06C21D": {
+ "Properties": {
+ "CompatibleArchitectures": [
+ "x86_64",
+ "arm64"
+ ],
+ "CompatibleRuntimes": [
+ "python3.8",
+ "python3.9",
+ "python3.10"
+ ],
+ "Content": {
+ "S3Bucket": {
+ "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}"
+ },
+ "S3Key": "test"
+ }
+ },
+ "Type": "AWS::Lambda::LayerVersion"
+ }
+ },
+ "Rules": {
+ "CheckBootstrapVersion": {
+ "Assertions": [
+ {
+ "Assert": {
+ "Fn::Not": [
+ {
+ "Fn::Contains": [
+ [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5"
+ ],
+ {
+ "Ref": "BootstrapVersion"
+ }
+ ]
+ }
+ ]
+ },
+ "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
+ }
+ ]
+ }
+ }
+}
diff --git a/source/lib/cms_common/constructs/tests/__snapshots__/test_identity_provider_config/test_identity_provider_config_snapshot.json b/source/lib/cms_common/constructs/tests/__snapshots__/test_identity_provider_config/test_identity_provider_config_snapshot.json
new file mode 100644
index 00000000..3756f6f5
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/__snapshots__/test_identity_provider_config/test_identity_provider_config_snapshot.json
@@ -0,0 +1,43 @@
+{
+ "Parameters": {
+ "BootstrapVersion": {
+ "Default": "/cdk-bootstrap/hnb659fds/version",
+ "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
+ "Type": "AWS::SSM::Parameter::Value"
+ },
+ "IdentityProviderId": {
+ "ConstraintDescription": "The identity provider ID must be a minimum of 3 characters.",
+ "Default": "cms",
+ "Description": "The ID associated with the identity provider configurations used for validation and exchange.",
+ "MinLength": 3,
+ "Type": "String"
+ }
+ },
+ "Rules": {
+ "CheckBootstrapVersion": {
+ "Assertions": [
+ {
+ "Assert": {
+ "Fn::Not": [
+ {
+ "Fn::Contains": [
+ [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5"
+ ],
+ {
+ "Ref": "BootstrapVersion"
+ }
+ ]
+ }
+ ]
+ },
+ "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
+ }
+ ]
+ }
+ }
+}
diff --git a/source/lib/cms_common/constructs/tests/__snapshots__/test_lambda_dependencies/test_lambda_dependencies_snapshot.json b/source/lib/cms_common/constructs/tests/__snapshots__/test_lambda_dependencies/test_lambda_dependencies_snapshot.json
new file mode 100644
index 00000000..699aac93
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/__snapshots__/test_lambda_dependencies/test_lambda_dependencies_snapshot.json
@@ -0,0 +1,58 @@
+{
+ "Parameters": {
+ "BootstrapVersion": {
+ "Default": "/cdk-bootstrap/hnb659fds/version",
+ "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
+ "Type": "AWS::SSM::Parameter::Value"
+ }
+ },
+ "Resources": {
+ "testlambdadependencieslambdadependencylayerversionAA06C21D": {
+ "Properties": {
+ "CompatibleArchitectures": [
+ "x86_64",
+ "arm64"
+ ],
+ "CompatibleRuntimes": [
+ "python3.8",
+ "python3.9",
+ "python3.10"
+ ],
+ "Content": {
+ "S3Bucket": {
+ "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}"
+ },
+ "S3Key": "test"
+ }
+ },
+ "Type": "AWS::Lambda::LayerVersion"
+ }
+ },
+ "Rules": {
+ "CheckBootstrapVersion": {
+ "Assertions": [
+ {
+ "Assert": {
+ "Fn::Not": [
+ {
+ "Fn::Contains": [
+ [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5"
+ ],
+ {
+ "Ref": "BootstrapVersion"
+ }
+ ]
+ }
+ ]
+ },
+ "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
+ }
+ ]
+ }
+ }
+}
diff --git a/source/lib/cms_common/constructs/tests/__snapshots__/test_vpc_construct/test_vpc_construct.json b/source/lib/cms_common/constructs/tests/__snapshots__/test_vpc_construct/test_vpc_construct.json
new file mode 100644
index 00000000..1d115e68
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/__snapshots__/test_vpc_construct/test_vpc_construct.json
@@ -0,0 +1,36 @@
+{
+ "Parameters": {
+ "BootstrapVersion": {
+ "Default": "/cdk-bootstrap/hnb659fds/version",
+ "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
+ "Type": "AWS::SSM::Parameter::Value"
+ }
+ },
+ "Rules": {
+ "CheckBootstrapVersion": {
+ "Assertions": [
+ {
+ "Assert": {
+ "Fn::Not": [
+ {
+ "Fn::Contains": [
+ [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5"
+ ],
+ {
+ "Ref": "BootstrapVersion"
+ }
+ ]
+ }
+ ]
+ },
+ "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
+ }
+ ]
+ }
+ }
+}
diff --git a/source/lib/cms_common/constructs/tests/fixture_constructs.py b/source/lib/cms_common/constructs/tests/fixture_constructs.py
new file mode 100644
index 00000000..52b8eeff
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/fixture_constructs.py
@@ -0,0 +1,253 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import tempfile
+from os.path import abspath, dirname
+from typing import Any, Dict, List
+from unittest.mock import MagicMock
+
+# Third Party Libraries
+import pytest
+from syrupy.extensions.json import JSONSnapshotExtension
+from syrupy.matchers import path_value
+from syrupy.types import SerializableData
+
+# AWS Libraries
+from aws_cdk import Stack, assertions, aws_lambda
+
+# Connected Mobility Solution on AWS
+from ..app_unique_id import AppUniqueId
+from ..cdk_lambda_vpc_config_construct import CDKLambdasVpcConfigConstruct
+from ..custom_resource_lambda import CustomResourceLambdaConstruct
+from ..identity_provider_config import IdentityProviderConfig
+from ..lambda_dependencies import LambdaDependenciesConstruct, LambdaDependencyError
+from ..vpc_construct import VpcConfig, VpcConstruct
+
+
+@pytest.fixture(name="snapshot_json_with_matcher")
+def fixture_snapshot_json_with_matcher(snapshot: SerializableData) -> SerializableData:
+ matcher = path_value(
+ mapping={
+ ".*": r"(\/?([0-9a-fA-F]+)\.zip|[a-zA-Z0-9:/-]+([0-9]{12})[a-zA-Z0-9:/-]+)",
+ },
+ regex=True,
+ types=(object,),
+ replacer=lambda data, match: data.replace(match[1], "test") if match else data,
+ )
+ return snapshot.use_extension(JSONSnapshotExtension)(matcher=matcher)
+
+
+@pytest.fixture(name="app_unique_id_stack", scope="module")
+def fixture_app_unique_id_stack() -> assertions.Template:
+ stack = Stack()
+ AppUniqueId.create_cfn_parameter(
+ stack,
+ )
+ return assertions.Template.from_stack(stack)
+
+
+@pytest.fixture(name="identity_provider_config_stack", scope="module")
+def fixture_identity_provider_config_stack() -> assertions.Template:
+ stack = Stack()
+ IdentityProviderConfig.create_cfn_parameter(
+ stack,
+ )
+ return assertions.Template.from_stack(stack)
+
+
+@pytest.fixture(name="app_unique_id_cfn_parameter", scope="module")
+def fixture_app_unique_id_cfn_parameter(
+ app_unique_id_stack: assertions.Template,
+) -> Any:
+ return dict(app_unique_id_stack.to_json()["Parameters"])["AppUniqueId"]
+
+
+@pytest.fixture(name="empty_lambda_dependencies_stack", scope="module")
+def fixture_empty_lambda_dependencies_stack() -> assertions.Template:
+ with tempfile.TemporaryDirectory() as tmpdirname:
+ # mock lambda code asset path
+ aws_lambda.Code.from_asset = MagicMock( # type: ignore[method-assign]
+ return_value=aws_lambda.AssetCode(path=tmpdirname)
+ )
+
+ stack = Stack()
+ LambdaDependenciesConstruct(
+ stack,
+ "test-lambda-dependencies",
+ pipfile_path=f"{dirname(abspath(__file__))}/test_pipfile_empty.toml",
+ dependency_layer_path=f"{dirname(abspath(__file__))}/mock_dependency_layer",
+ )
+ return assertions.Template.from_stack(stack)
+
+
+@pytest.fixture(name="populated_lambda_dependencies_stack", scope="module")
+def fixture_populated_lambda_dependencies_stack() -> assertions.Template:
+ with tempfile.TemporaryDirectory() as tmpdirname:
+ # mock lambda code asset path
+ aws_lambda.Code.from_asset = MagicMock( # type: ignore[method-assign]
+ return_value=aws_lambda.AssetCode(path=tmpdirname)
+ )
+
+ stack = Stack()
+ try:
+ LambdaDependenciesConstruct(
+ stack,
+ "test-lambda-dependencies",
+ pipfile_path=f"{dirname(abspath(__file__))}/test_pipfile_populated.toml",
+ dependency_layer_path=f"{dirname(abspath(__file__))}/mock_dependency_layer",
+ )
+ except LambdaDependencyError:
+ pass # Error excpected because dependencies are not real
+ return assertions.Template.from_stack(stack)
+
+
+@pytest.fixture(name="custom_resource_lambda_stack", scope="module")
+def fixture_custom_resource_lambda_stack() -> assertions.Template:
+ with tempfile.TemporaryDirectory() as tmpdirname:
+ # mock lambda code asset path
+ aws_lambda.Code.from_asset = MagicMock( # type: ignore[method-assign]
+ return_value=aws_lambda.AssetCode(path=tmpdirname)
+ )
+
+ stack = Stack()
+
+ vpc_construct = VpcConstruct(
+ stack,
+ "test-vpc-construct",
+ vpc_config=VpcConfig(
+ vpc_name="test-vpc-name",
+ vpc_id="test-vpc-id",
+ public_subnets=["test-vpc-public-subnet-1", "test-vpc-public-subnet-2"],
+ private_subnets=[
+ "test-vpc-private-subnet-1",
+ "test-vpc-private-subnet-2",
+ ],
+ isolated_subnets=[
+ "test-vpc-isolated-subnet-1",
+ "test-vpc-isolated-subnet-2",
+ ],
+ availability_zones=["us-east-1", "us-east-2"],
+ ),
+ )
+
+ lambda_dependencies = LambdaDependenciesConstruct(
+ stack,
+ "test-lambda-dependencies",
+ pipfile_path=f"{dirname(abspath(__file__))}/test_pipfile_empty.toml",
+ dependency_layer_path=f"{dirname(abspath(__file__))}/mock_dependency_layer",
+ )
+ CustomResourceLambdaConstruct(
+ stack,
+ "test-custom-resource-lambda",
+ dependency_layer=lambda_dependencies.dependency_layer,
+ asset_path="dist/lambda/custom_resource.zip",
+ unique_id="test-id",
+ name="test-module-name",
+ user_agent_string="test-user-agent-string",
+ vpc_construct=vpc_construct,
+ )
+ return assertions.Template.from_stack(stack)
+
+
+@pytest.fixture(name="cdk_lambda_vpc_config_construct_stack_template", scope="module")
+def fixture_cdk_lambda_vpc_config_construct_stack_template() -> assertions.Template:
+ stack = Stack()
+
+ vpc_construct = VpcConstruct(
+ stack,
+ "test-vpc-construct",
+ vpc_config=VpcConfig(
+ vpc_name="test-vpc-name",
+ vpc_id="test-vpc-id",
+ public_subnets=["test-vpc-public-subnet-1", "test-vpc-public-subnet-2"],
+ private_subnets=[
+ "test-vpc-private-subnet-1",
+ "test-vpc-private-subnet-2",
+ ],
+ isolated_subnets=[
+ "test-vpc-isolated-subnet-1",
+ "test-vpc-isolated-subnet-2",
+ ],
+ availability_zones=["us-east-1", "us-east-2"],
+ ),
+ )
+
+ CDKLambdasVpcConfigConstruct(
+ stack,
+ "test-cdk-lambda-vpc-config-construct-lambda",
+ vpc_construct=vpc_construct,
+ subnets=[
+ "test-vpc-private-subnet-1",
+ "test-vpc-private-subnet-2",
+ ],
+ )
+ return assertions.Template.from_stack(stack)
+
+
+@pytest.fixture(name="vpc_construct_stack_template", scope="module")
+def fixture_vpc_construct_stack_template() -> assertions.Template:
+ with tempfile.TemporaryDirectory():
+ stack = Stack()
+ VpcConstruct(
+ stack,
+ "test-vpc-construct",
+ vpc_config=VpcConfig(
+ vpc_name="test-vpc-name",
+ vpc_id="test-vpc-id",
+ public_subnets=["test-vpc-public-subnet-1", "test-vpc-public-subnet-2"],
+ private_subnets=[
+ "test-vpc-private-subnet-1",
+ "test-vpc-private-subnet-2",
+ ],
+ isolated_subnets=[
+ "test-vpc-isolated-subnet-1",
+ "test-vpc-isolated-subnet-2",
+ ],
+ availability_zones=["us-east-1", "us-east-2"],
+ ),
+ )
+
+ return assertions.Template.from_stack(stack)
+
+
+@pytest.fixture(name="vpc_construct", scope="module")
+def fixture_vpc_construct_stack() -> VpcConstruct:
+ with tempfile.TemporaryDirectory():
+ stack = Stack()
+ vpc_construct = VpcConstruct(
+ stack,
+ "test-vpc-construct",
+ vpc_config=VpcConfig(
+ vpc_name="test-vpc-name",
+ vpc_id="test-vpc-id",
+ public_subnets=["test-vpc-public-subnet-1", "test-vpc-public-subnet-2"],
+ private_subnets=[
+ "test-vpc-private-subnet-1",
+ "test-vpc-private-subnet-2",
+ ],
+ isolated_subnets=[
+ "test-vpc-isolated-subnet-1",
+ "test-vpc-isolated-subnet-2",
+ ],
+ availability_zones=["us-east-1", "us-east-2"],
+ ),
+ )
+
+ return vpc_construct
+
+
+@pytest.fixture(name="subnet_selections", scope="module")
+def fixture_vpc_construct_subnet_selections() -> Dict[str, List[str]]:
+ return {
+ "public_subnets": ["test-vpc-public-subnet-1", "test-vpc-public-subnet-2"],
+ "private_subnets": [
+ "test-vpc-private-subnet-1",
+ "test-vpc-private-subnet-2",
+ ],
+ "isolated_subnets": [
+ "test-vpc-isolated-subnet-1",
+ "test-vpc-isolated-subnet-2",
+ ],
+ }
diff --git a/source/lib/cms_common/constructs/tests/test_app_registry.py b/source/lib/cms_common/constructs/tests/test_app_registry.py
new file mode 100644
index 00000000..e24e6230
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/test_app_registry.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Third Party Libraries
+from syrupy.types import SerializableData
+
+# AWS Libraries
+from aws_cdk import Stack, assertions
+
+# Connected Mobility Solution on AWS
+from ..app_registry import AppRegistryConstruct, AppRegistryInputs
+
+
+def test_app_registry_snapshot(
+ snapshot_json_with_matcher: SerializableData,
+) -> None:
+ stack = Stack()
+ AppRegistryConstruct(
+ stack,
+ "test-app-registry",
+ app_registry_inputs=AppRegistryInputs(
+ application_name="test-application-name",
+ application_type="test-application-type",
+ solution_id="test-solution-id",
+ solution_name="test-solution-name",
+ solution_version="test-solution-version",
+ ),
+ )
+ assert assertions.Template.from_stack(stack).to_json() == snapshot_json_with_matcher
diff --git a/source/lib/cms_common/constructs/tests/test_app_unique_id.py b/source/lib/cms_common/constructs/tests/test_app_unique_id.py
new file mode 100644
index 00000000..49387983
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/test_app_unique_id.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+# Standard Library
+import re
+from typing import Any, Dict
+
+# Third Party Libraries
+import pytest
+from syrupy.types import SerializableData
+
+# AWS Libraries
+from aws_cdk import assertions
+
+
+def test_app_unique_id_snapshot(
+ app_unique_id_stack: assertions.Template,
+ snapshot_json_with_matcher: SerializableData,
+) -> None:
+ assert app_unique_id_stack.to_json() == snapshot_json_with_matcher
+
+
+@pytest.mark.parametrize(
+ "cfn_parameter_value, is_valid",
+ [
+ ("abc", True),
+ ("abcdefghij", True),
+ ("ab1", True),
+ ("ab-1", True),
+ ("1a-2b-3c", True),
+ ("ab", False), # too short
+ ("abcdefghijk", False), # too long
+ ("abC", False), # uppercase not allowed
+ ("ab#", False), # special character not allowed
+ ("ab_cd", False), # underscore not allowed
+ ],
+)
+def test_app_unique_id_allowed_values(
+ app_unique_id_cfn_parameter: Dict[str, Any],
+ cfn_parameter_value: str,
+ is_valid: bool,
+) -> None:
+ def validate(value: str) -> bool:
+ if (
+ len(value) < app_unique_id_cfn_parameter["MinLength"]
+ or len(value) > app_unique_id_cfn_parameter["MaxLength"]
+ ):
+ return False
+ match = re.match(app_unique_id_cfn_parameter["AllowedPattern"], value)
+ return match is not None
+
+ assert validate(cfn_parameter_value) == is_valid
diff --git a/source/lib/cms_common/constructs/tests/test_cdk_lambda_vpc_config_construct.py b/source/lib/cms_common/constructs/tests/test_cdk_lambda_vpc_config_construct.py
new file mode 100644
index 00000000..623242e6
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/test_cdk_lambda_vpc_config_construct.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+
+# Third Party Libraries
+from syrupy.types import SerializableData
+
+# AWS Libraries
+from aws_cdk import assertions
+
+
+def test_cdk_lambda_vpc_config_construct(
+ cdk_lambda_vpc_config_construct_stack_template: assertions.Template,
+ snapshot_json_with_matcher: SerializableData,
+) -> None:
+ assert (
+ cdk_lambda_vpc_config_construct_stack_template.to_json()
+ == snapshot_json_with_matcher
+ )
diff --git a/source/lib/cms_common/constructs/tests/test_custom_resource_lambda.py b/source/lib/cms_common/constructs/tests/test_custom_resource_lambda.py
new file mode 100644
index 00000000..93ef26de
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/test_custom_resource_lambda.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Third Party Libraries
+from syrupy.types import SerializableData
+
+# AWS Libraries
+from aws_cdk import assertions
+
+
+def test_custom_resource_lambda_snapshot(
+ custom_resource_lambda_stack: assertions.Template,
+ snapshot_json_with_matcher: SerializableData,
+) -> None:
+
+ assert custom_resource_lambda_stack.to_json() == snapshot_json_with_matcher
diff --git a/source/lib/cms_common/constructs/tests/test_identity_provider_config.py b/source/lib/cms_common/constructs/tests/test_identity_provider_config.py
new file mode 100644
index 00000000..226ae0e6
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/test_identity_provider_config.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+# Standard Library
+
+# Third Party Libraries
+from syrupy.types import SerializableData
+
+# AWS Libraries
+from aws_cdk import assertions
+
+
+def test_identity_provider_config_snapshot(
+ identity_provider_config_stack: assertions.Template,
+ snapshot_json_with_matcher: SerializableData,
+) -> None:
+ assert identity_provider_config_stack.to_json() == snapshot_json_with_matcher
diff --git a/source/lib/cms_common/constructs/tests/test_lambda_dependencies.py b/source/lib/cms_common/constructs/tests/test_lambda_dependencies.py
new file mode 100644
index 00000000..b6519d7e
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/test_lambda_dependencies.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from os.path import abspath, dirname
+
+# Third Party Libraries
+from syrupy.types import SerializableData
+
+# AWS Libraries
+from aws_cdk import assertions
+
+
+def test_lambda_dependencies_snapshot(
+ empty_lambda_dependencies_stack: assertions.Template,
+ snapshot_json_with_matcher: SerializableData,
+) -> None:
+
+ assert empty_lambda_dependencies_stack.to_json() == snapshot_json_with_matcher
+
+
+def test_requiremments_file_correctly_populated(
+ populated_lambda_dependencies_stack: assertions.Template,
+) -> None:
+ with open(
+ f"{dirname(abspath(__file__))}/mock_dependency_layer/requirements.txt",
+ "r",
+ encoding="utf-8",
+ ) as req_file:
+ generated_requirements = set(req_file.read().splitlines())
+
+ expected_requirements = set(
+ ["package_a >=2.28.1", "package_b", "package_c[essential] ", "./../lib"]
+ )
+
+ assert generated_requirements == expected_requirements
diff --git a/source/lib/cms_common/constructs/tests/test_pipfile_empty.toml b/source/lib/cms_common/constructs/tests/test_pipfile_empty.toml
new file mode 100644
index 00000000..c398b0d5
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/test_pipfile_empty.toml
@@ -0,0 +1,11 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+
+[dev-packages]
+
+[requires]
+python_version = "3.10"
diff --git a/source/lib/cms_common/constructs/tests/test_pipfile_populated.toml b/source/lib/cms_common/constructs/tests/test_pipfile_populated.toml
new file mode 100644
index 00000000..2daee646
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/test_pipfile_populated.toml
@@ -0,0 +1,15 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+package_a = ">=2.28.1"
+package_b = "*"
+package_c = {extras = ["essential"], version = "*"}
+cms_common = {path = "./../lib", editable = true}
+
+[dev-packages]
+
+[requires]
+python_version = "3.10"
diff --git a/source/lib/cms_common/constructs/tests/test_vpc_construct.py b/source/lib/cms_common/constructs/tests/test_vpc_construct.py
new file mode 100644
index 00000000..208acf7d
--- /dev/null
+++ b/source/lib/cms_common/constructs/tests/test_vpc_construct.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from typing import Dict, List
+
+# Third Party Libraries
+from syrupy.types import SerializableData
+
+# AWS Libraries
+from aws_cdk import assertions
+
+# Connected Mobility Solution on AWS
+from ..vpc_construct import VpcConstruct
+
+
+def test_vpc_construct(
+ vpc_construct_stack_template: assertions.Template,
+ snapshot_json_with_matcher: SerializableData,
+) -> None:
+ assert vpc_construct_stack_template.to_json() == snapshot_json_with_matcher
+
+
+def test_vpc_select_subnets(
+ vpc_construct: VpcConstruct, subnet_selections: Dict[str, List[str]]
+) -> None:
+ private_subnet_selection = vpc_construct.vpc.select_subnets(
+ selection=vpc_construct.private_subnet_selection
+ )
+ assert private_subnet_selection["subnetIds"] == subnet_selections["private_subnets"]
+
+ public_subnet_selection = vpc_construct.vpc.select_subnets(
+ selection=vpc_construct.public_subnet_selection
+ )
+ assert public_subnet_selection["subnetIds"] == subnet_selections["public_subnets"]
+
+ isolated_subnet_selection = vpc_construct.vpc.select_subnets(
+ selection=vpc_construct.isolated_subnet_selection
+ )
+ assert (
+ isolated_subnet_selection["subnetIds"] == subnet_selections["isolated_subnets"]
+ )
diff --git a/source/lib/cms_common/constructs/vpc_construct.py b/source/lib/cms_common/constructs/vpc_construct.py
new file mode 100644
index 00000000..54d9be40
--- /dev/null
+++ b/source/lib/cms_common/constructs/vpc_construct.py
@@ -0,0 +1,282 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from typing import Any, Dict, List
+
+# Third Party Libraries
+import jsii
+from attrs import define
+
+# AWS Libraries
+from aws_cdk import Annotations, CustomResource, Stack, aws_ec2, aws_iam
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..config.resource_names import ResourceName
+from ..config.ssm import resolve_ssm_parameter
+from ..enums.aws_resource_lookup import AwsResourceLookupCustomResourceType
+from ..resource_names.config import ConfigResourceNames
+
+
+def get_vpc_name(scope: Construct, app_unique_id: str) -> str:
+ config_resource_names = ConfigResourceNames.from_app_unique_id(app_unique_id)
+
+ aws_resource_lookup_lambda_arn = resolve_ssm_parameter(
+ parameter_name=config_resource_names.aws_resource_lookup_lambda_arn_ssm_parameter
+ )
+
+ vpc_name_custom_resource = CustomResource(
+ scope,
+ "vpc-name-custom-resource",
+ service_token=aws_resource_lookup_lambda_arn,
+ resource_type=f"Custom::{AwsResourceLookupCustomResourceType.SSM_PARAMETERS.value}",
+ properties={
+ "Resource": AwsResourceLookupCustomResourceType.SSM_PARAMETERS.value,
+ "ParameterName": config_resource_names.vpc_name_ssm_parameter,
+ },
+ )
+
+ return vpc_name_custom_resource.get_att_string("parameter_value")
+
+
+@define(auto_attribs=True, frozen=True)
+class VpcConfig:
+ vpc_name: str
+ vpc_id: str
+ public_subnets: List[str]
+ private_subnets: List[str]
+ isolated_subnets: List[str]
+ availability_zones: List[str]
+
+
+def create_vpc_config(vpc_name: str) -> VpcConfig:
+ vpc_ssm_prefix = f"/solution/vpc/{vpc_name}"
+ return VpcConfig(
+ vpc_name=vpc_name,
+ vpc_id=resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="vpcid"
+ )
+ ),
+ public_subnets=[
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/public/1"
+ )
+ ),
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/public/2"
+ )
+ ),
+ ],
+ private_subnets=[
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/private/1"
+ )
+ ),
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/private/2"
+ )
+ ),
+ ],
+ isolated_subnets=[
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/isolated/1"
+ )
+ ),
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/isolated/2"
+ )
+ ),
+ ],
+ availability_zones=[
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="azs/1"
+ )
+ ),
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="azs/2"
+ )
+ ),
+ ],
+ )
+
+
+class IncorrectSubnetType(Exception):
+ ...
+
+
+@jsii.implements(aws_ec2.IVpc)
+class UnsafeDynamicVpc(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ vpc_id: str,
+ vpc_name: str,
+ public_subnets: List[aws_ec2.ISubnet],
+ private_subnets: List[aws_ec2.ISubnet],
+ isolated_subnets: List[aws_ec2.ISubnet],
+ availability_zones: List[str],
+ ) -> None:
+ super().__init__(scope, construct_id)
+ self._vpc_id = vpc_id
+ self._vpc_name = vpc_name
+ self._vpc_arn = Stack.of(self).format_arn(
+ service="ec2", resource="vpc", resource_name=self.vpc_id
+ )
+
+ self._public_subnets = public_subnets
+ self._private_subnets = private_subnets
+ self._isolated_subnets = isolated_subnets
+ self._availability_zones = availability_zones
+
+ @property
+ def vpc_id(self) -> str:
+ return self._vpc_id
+
+ @property
+ def availability_zones(self) -> List[str]:
+ return self._availability_zones
+
+ @property
+ def public_subnets(self) -> List[aws_ec2.ISubnet]:
+ return self._public_subnets
+
+ @property
+ def private_subnets(self) -> List[aws_ec2.ISubnet]:
+ return self._private_subnets
+
+ @property
+ def isolated_subnets(self) -> List[aws_ec2.ISubnet]:
+ return self._isolated_subnets
+
+ @property
+ def vpc_arn(self) -> str:
+ return self._vpc_arn
+
+ def select_subnets(self, selection: aws_ec2.SubnetSelection) -> Dict[str, Any]:
+ ### As of now this function only supports selection of subnet by types
+ selected_subnets = None
+
+ has_public = False
+ match (selection.subnet_type):
+ case aws_ec2.SubnetType.PUBLIC:
+ selected_subnets = self._public_subnets
+ has_public = True
+ case aws_ec2.SubnetType.PRIVATE_WITH_EGRESS:
+ selected_subnets = self._private_subnets
+ case aws_ec2.SubnetType.PRIVATE_ISOLATED:
+ selected_subnets = self._isolated_subnets
+
+ if not selected_subnets:
+ raise IncorrectSubnetType
+
+ internet_connectivity_established = aws_iam.CompositeDependable(
+ *[subnet.internet_connectivity_established for subnet in selected_subnets]
+ )
+ return {
+ "subnetIds": [subnet.subnet_id for subnet in selected_subnets],
+ "availabilityZones": self._availability_zones,
+ "hasPublic": has_public,
+ "subnets": selected_subnets,
+ "internetConnectivityEstablished": internet_connectivity_established,
+ }
+
+
+class VpcConstruct(Construct):
+ def __init__(
+ self, scope: Construct, construct_id: str, vpc_config: VpcConfig
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ self.public_subnets = [
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "public-subnet-1",
+ subnet_id=vpc_config.public_subnets[0],
+ ),
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "public-subnet-2",
+ subnet_id=vpc_config.public_subnets[1],
+ ),
+ ]
+ Annotations.of(self.public_subnets[0]).acknowledge_warning(
+ "@aws-cdk/aws-ec2:noSubnetRouteTableId"
+ )
+ Annotations.of(self.public_subnets[1]).acknowledge_warning(
+ "@aws-cdk/aws-ec2:noSubnetRouteTableId"
+ )
+
+ self.public_subnet_selection = aws_ec2.SubnetSelection(
+ subnets=self.public_subnets, subnet_type=aws_ec2.SubnetType.PUBLIC
+ )
+
+ self.private_subnets = [
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "private-subnet-1",
+ subnet_id=vpc_config.private_subnets[0],
+ ),
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "private-subnet-2",
+ subnet_id=vpc_config.private_subnets[1],
+ ),
+ ]
+ Annotations.of(self.private_subnets[0]).acknowledge_warning(
+ "@aws-cdk/aws-ec2:noSubnetRouteTableId"
+ )
+ Annotations.of(self.private_subnets[1]).acknowledge_warning(
+ "@aws-cdk/aws-ec2:noSubnetRouteTableId"
+ )
+
+ self.private_subnet_selection = aws_ec2.SubnetSelection(
+ subnets=self.private_subnets,
+ subnet_type=aws_ec2.SubnetType.PRIVATE_WITH_EGRESS,
+ )
+
+ self.isolated_subnets = [
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "isolated-subnet-1",
+ subnet_id=vpc_config.isolated_subnets[0],
+ ),
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "isolated-subnet-2",
+ subnet_id=vpc_config.isolated_subnets[1],
+ ),
+ ]
+ Annotations.of(self.isolated_subnets[0]).acknowledge_warning(
+ "@aws-cdk/aws-ec2:noSubnetRouteTableId"
+ )
+ Annotations.of(self.isolated_subnets[1]).acknowledge_warning(
+ "@aws-cdk/aws-ec2:noSubnetRouteTableId"
+ )
+
+ self.isolated_subnet_selection = aws_ec2.SubnetSelection(
+ subnets=self.isolated_subnets,
+ subnet_type=aws_ec2.SubnetType.PRIVATE_ISOLATED,
+ )
+
+ self.vpc = UnsafeDynamicVpc(
+ self,
+ "cms-vpc",
+ vpc_id=vpc_config.vpc_id,
+ vpc_name=vpc_config.vpc_name,
+ public_subnets=self.public_subnets,
+ private_subnets=self.private_subnets,
+ isolated_subnets=self.isolated_subnets,
+ availability_zones=vpc_config.availability_zones,
+ )
diff --git a/source/lib/cms_common/constructs/vpc_prefix_list_lookup_custom_resource.py b/source/lib/cms_common/constructs/vpc_prefix_list_lookup_custom_resource.py
new file mode 100644
index 00000000..7813b3ba
--- /dev/null
+++ b/source/lib/cms_common/constructs/vpc_prefix_list_lookup_custom_resource.py
@@ -0,0 +1,170 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# AWS Libraries
+from aws_cdk import RemovalPolicy, Stack, aws_iam, aws_logs, custom_resources
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..aspects.nag_suppression import NagSuppression, NagType
+from ..config.resource_names import ResourceName, ResourcePrefix
+from ..policy_generators.cloudwatch import (
+ generate_lambda_cloudwatch_logs_policy_document,
+)
+from ..policy_generators.ec2_vpc import generate_ec2_vpc_policy
+from .vpc_construct import VpcConstruct
+
+
+class VpcPrefixListLookupCustomResourceConstruct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ app_unique_id: str,
+ module_name: str,
+ vpc_construct: VpcConstruct,
+ prefix_list_name: str,
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ function_name = ResourceName.hyphen_separated(
+ ResourcePrefix.hyphen_separated(app_unique_id, module_name),
+ "vpc-prefix-list-lookup",
+ )
+
+ role = aws_iam.Role(
+ self,
+ "vpc-prefix-list-custom-resource-role",
+ assumed_by=aws_iam.ServicePrincipal("lambda.amazonaws.com"),
+ inline_policies={
+ "lambda-logs-policy": generate_lambda_cloudwatch_logs_policy_document(
+ self, function_name
+ ),
+ "ec2-policy": generate_ec2_vpc_policy(
+ self,
+ vpc_construct=vpc_construct,
+ subnet_selection=vpc_construct.private_subnet_selection,
+ authorized_service="lambda.amazonaws.com",
+ ),
+ "ec2-prefix-list-policy": aws_iam.PolicyDocument(
+ statements=[
+ aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=[
+ "ec2:DescribeManagedPrefixLists",
+ ],
+ resources=["*"],
+ conditions={
+ "StringEquals": {
+ "aws:PrincipalAccount": [Stack.of(self).account],
+ "aws:RequestedRegion": [Stack.of(self).region],
+ }
+ },
+ )
+ ]
+ ),
+ },
+ )
+
+ prefix_list_lookup = custom_resources.AwsCustomResource(
+ self,
+ "vpc-endpoint-prefix-list-custom-resource",
+ function_name=function_name,
+ vpc=vpc_construct.vpc, # type: ignore[arg-type]
+ vpc_subnets=vpc_construct.private_subnet_selection,
+ log_retention=aws_logs.RetentionDays.THREE_MONTHS,
+ removal_policy=RemovalPolicy.DESTROY,
+ on_create=custom_resources.AwsSdkCall(
+ service="EC2",
+ action="describeManagedPrefixLists",
+ physical_resource_id=custom_resources.PhysicalResourceId.from_response(
+ "PrefixLists.0.PrefixListId"
+ ),
+ parameters={
+ "Filters": [
+ {"Name": "prefix-list-name", "Values": [prefix_list_name]}
+ ]
+ },
+ ),
+ role=role,
+ install_latest_aws_sdk=False,
+ )
+
+ self.prefix_list_id = prefix_list_lookup.get_response_field(
+ "PrefixLists.0.PrefixListId"
+ )
+
+ NagSuppression.add_inline_suppression(
+ node=role.node.default_child,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "reason": "Wildcard permissions required to write to log streams.",
+ },
+ ]
+ },
+ nag_type=NagType.CDK_NAG,
+ )
+ NagSuppression.add_inline_suppression(
+ node=role.node.default_child,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "W11",
+ "reason": "Wildcard needed for log policy",
+ },
+ ]
+ },
+ nag_type=NagType.CFN_NAG,
+ )
+
+ # Workaround for the fact that AwsCustomResource doesn't expose resources in any manner in its tree
+ provider_function = Stack.of(self).node.find_child(
+ f'AWS{prefix_list_lookup.PROVIDER_FUNCTION_UUID.replace("-", "")}'
+ )
+ lambda_function = provider_function.node.find_child("Resource")
+ lambda_function_security_group = provider_function.node.find_child(
+ "SecurityGroup"
+ ).node.default_child
+ NagSuppression.add_inline_suppression(
+ lambda_function,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-L1",
+ "reason": "Log retention lambda uses policies that require wildcard permissions",
+ },
+ ]
+ },
+ nag_type=NagType.CDK_NAG,
+ )
+ NagSuppression.add_inline_suppression(
+ node=lambda_function,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "W92",
+ "reason": "No reserved concurrency required for custom resources",
+ },
+ ]
+ },
+ nag_type=NagType.CFN_NAG,
+ )
+ NagSuppression.add_inline_suppression(
+ node=lambda_function_security_group,
+ suppression={
+ "rules_to_suppress": [
+ {
+ "id": "W5",
+ "reason": "Unable to know egress requirement. leaving open for now",
+ },
+ {
+ "id": "W40",
+ "reason": "Unable to know egress requirement. leaving open for now",
+ },
+ ]
+ },
+ nag_type=NagType.CFN_NAG,
+ )
diff --git a/source/lib/cms_common/enums/__init__.py b/source/lib/cms_common/enums/__init__.py
new file mode 100644
index 00000000..9122fb9e
--- /dev/null
+++ b/source/lib/cms_common/enums/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from .aws_resource_lookup import AwsResourceLookupCustomResourceType
+from .custom_resource import CustomResourceRequestType, CustomResourceStatusType
+from .rotate_secret import RotateSecretStep, SecretStatus
diff --git a/source/lib/cms_common/enums/aws_resource_lookup.py b/source/lib/cms_common/enums/aws_resource_lookup.py
new file mode 100644
index 00000000..1e75184d
--- /dev/null
+++ b/source/lib/cms_common/enums/aws_resource_lookup.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from enum import Enum
+
+
+class AwsResourceLookupCustomResourceType(Enum):
+ SSM_PARAMETERS = "SsmParameters"
diff --git a/source/lib/cms_common/enums/custom_resource.py b/source/lib/cms_common/enums/custom_resource.py
new file mode 100644
index 00000000..44edc9a0
--- /dev/null
+++ b/source/lib/cms_common/enums/custom_resource.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from enum import Enum
+
+
+class CustomResourceRequestType(Enum):
+ CREATE = "Create"
+ UPDATE = "Update"
+ DELETE = "Delete"
+
+
+class CustomResourceStatusType(Enum):
+ SUCCESS = "SUCCESS"
+ FAILED = "FAILED"
diff --git a/templates/modules/cms_ev_battery_health_on_aws/v1/instance_infrastructure/source/handlers/rotate_secret/lib/rotate_secret_enum.py b/source/lib/cms_common/enums/rotate_secret.py
similarity index 100%
rename from templates/modules/cms_ev_battery_health_on_aws/v1/instance_infrastructure/source/handlers/rotate_secret/lib/rotate_secret_enum.py
rename to source/lib/cms_common/enums/rotate_secret.py
diff --git a/source/lib/cms_common/policy_generators/__init__.py b/source/lib/cms_common/policy_generators/__init__.py
new file mode 100644
index 00000000..55d7470d
--- /dev/null
+++ b/source/lib/cms_common/policy_generators/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from .cloudwatch import generate_lambda_cloudwatch_logs_policy_document
+from .ec2_vpc import generate_ec2_vpc_policy
+from .kms import generate_kms_policy_statement
diff --git a/source/lib/cms_common/policy_generators/cloudwatch.py b/source/lib/cms_common/policy_generators/cloudwatch.py
new file mode 100644
index 00000000..30186907
--- /dev/null
+++ b/source/lib/cms_common/policy_generators/cloudwatch.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+
+# AWS Libraries
+from aws_cdk import ArnFormat, Stack, aws_iam
+from constructs import Construct
+
+
+def generate_lambda_cloudwatch_logs_policy_document(
+ self: Construct, lambda_function_name: str
+) -> aws_iam.PolicyDocument:
+ return aws_iam.PolicyDocument(
+ statements=[
+ aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=[
+ "logs:CreateLogGroup",
+ "logs:CreateLogStream",
+ "logs:PutLogEvents",
+ ],
+ resources=[
+ Stack.of(self).format_arn(
+ service="logs",
+ resource="log-group",
+ resource_name=f"/aws/lambda/{lambda_function_name}",
+ arn_format=ArnFormat.COLON_RESOURCE_NAME,
+ ),
+ Stack.of(self).format_arn(
+ service="logs",
+ resource="log-group",
+ resource_name=f"/aws/lambda/{lambda_function_name}:log-stream:*",
+ arn_format=ArnFormat.COLON_RESOURCE_NAME,
+ ),
+ ],
+ ),
+ ]
+ )
diff --git a/source/lib/cms_common/policy_generators/ec2_vpc.py b/source/lib/cms_common/policy_generators/ec2_vpc.py
new file mode 100644
index 00000000..69bf68bb
--- /dev/null
+++ b/source/lib/cms_common/policy_generators/ec2_vpc.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+# AWS Libraries
+
+# AWS Libraries
+from aws_cdk import Stack, aws_ec2, aws_iam
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..constructs.vpc_construct import VpcConstruct
+
+
+def generate_ec2_vpc_policy(
+ self: Construct,
+ vpc_construct: VpcConstruct,
+ subnet_selection: aws_ec2.SubnetSelection,
+ authorized_service: str,
+) -> aws_iam.PolicyDocument:
+ return aws_iam.PolicyDocument(
+ statements=[
+ aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=[
+ "ec2:CreateNetworkInterfacePermission",
+ ],
+ resources=[
+ Stack.of(self).format_arn(
+ partition=Stack.of(self).partition,
+ service="ec2",
+ region=Stack.of(self).region,
+ account=Stack.of(self).account,
+ resource="network-interface",
+ resource_name="*",
+ ),
+ ],
+ conditions={
+ "StringEquals": {
+ "ec2:Subnet": [
+ Stack.of(self).format_arn(
+ partition=Stack.of(self).partition,
+ service="ec2",
+ region=Stack.of(self).region,
+ account=Stack.of(self).account,
+ resource="subnet",
+ resource_name=subnet_id,
+ )
+ for subnet_id in vpc_construct.vpc.select_subnets( # type: ignore[union-attr]
+ subnet_selection
+ ).get(
+ "subnetIds"
+ )
+ ],
+ "ec2:AuthorizedService": authorized_service,
+ }
+ },
+ ),
+ aws_iam.PolicyStatement(
+ actions=[
+ "ec2:DescribeNetworkInterfaces",
+ "ec2:CreateNetworkInterface",
+ "ec2:DeleteNetworkInterface",
+ ],
+ effect=aws_iam.Effect.ALLOW,
+ resources=["*"],
+ ),
+ ]
+ )
diff --git a/source/lib/cms_common/policy_generators/kms.py b/source/lib/cms_common/policy_generators/kms.py
new file mode 100644
index 00000000..b4e021f8
--- /dev/null
+++ b/source/lib/cms_common/policy_generators/kms.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+
+# AWS Libraries
+from aws_cdk import ArnFormat, Stack, aws_iam
+from constructs import Construct
+
+
+def generate_kms_policy_statement(
+ self: Construct, kms_encryption_key_id: str, allow_encrypt: bool
+) -> aws_iam.PolicyStatement:
+ policy_permissions = ["kms:Decrypt"]
+ encrypt_permissions = ["kms:Encrypt", "kms:GenerateDataKey"]
+ if allow_encrypt:
+ policy_permissions.extend(encrypt_permissions)
+ return aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=policy_permissions,
+ resources=[
+ Stack.of(self).format_arn(
+ service="kms",
+ resource="key",
+ resource_name=f"{kms_encryption_key_id}",
+ arn_format=ArnFormat.SLASH_RESOURCE_NAME,
+ ),
+ ],
+ )
diff --git a/source/lib/cms_common/py.typed b/source/lib/cms_common/py.typed
new file mode 100644
index 00000000..9724ed56
--- /dev/null
+++ b/source/lib/cms_common/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561. The mypy package uses inline types.
diff --git a/source/lib/cms_common/resource_names/__init__.py b/source/lib/cms_common/resource_names/__init__.py
new file mode 100644
index 00000000..0e3de8b6
--- /dev/null
+++ b/source/lib/cms_common/resource_names/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Connected Mobility Solution on AWS
+from .auth import AuthResourceNames
+from .config import ConfigResourceNames
+from .module_short_names import CMSModuleShortNames
diff --git a/source/lib/cms_common/resource_names/auth.py b/source/lib/cms_common/resource_names/auth.py
new file mode 100644
index 00000000..89858b26
--- /dev/null
+++ b/source/lib/cms_common/resource_names/auth.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from dataclasses import dataclass
+
+# Connected Mobility Solution on AWS
+from .module_short_names import CMSModuleShortNames
+
+
+@dataclass(frozen=True)
+class AuthResourceNames:
+ auth_prefix: str
+ idp_config_secret: str
+ idp_config_secret_arn_ssm_parameter: str
+ client_config_secret: str
+ client_config_secret_arn_ssm_parameter: str
+ authorization_code_flow_config_secret: str
+ authorization_code_flow_config_secret_arn_ssm_parameter: str
+
+ @classmethod
+ def from_identity_provider_id(
+ cls, identity_provider_id: str
+ ) -> "AuthResourceNames":
+ auth_prefix = f"/solution/{CMSModuleShortNames.AUTH}"
+ auth_prefix_with_id = f"{auth_prefix}/{identity_provider_id}"
+ return AuthResourceNames(
+ auth_prefix=auth_prefix_with_id,
+ idp_config_secret=f"{auth_prefix_with_id}/idp-config",
+ idp_config_secret_arn_ssm_parameter=f"{auth_prefix_with_id}/idp-config/secret/arn",
+ client_config_secret=f"{auth_prefix_with_id}/client-config/default",
+ client_config_secret_arn_ssm_parameter=f"{auth_prefix_with_id}/client-config/default/secret/arn",
+ authorization_code_flow_config_secret=f"{auth_prefix_with_id}/authorization-code-flow/config",
+ authorization_code_flow_config_secret_arn_ssm_parameter=f"{auth_prefix_with_id}/authorization-code-flow/config/secret/arn",
+ )
diff --git a/source/lib/cms_common/resource_names/config.py b/source/lib/cms_common/resource_names/config.py
new file mode 100644
index 00000000..fc7c7e93
--- /dev/null
+++ b/source/lib/cms_common/resource_names/config.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from dataclasses import dataclass
+
+# Connected Mobility Solution on AWS
+from .module_short_names import CMSModuleShortNames
+
+
+@dataclass(frozen=True)
+class ConfigResourceNames:
+ config_prefix: str
+ aws_resource_lookup_lambda_arn_ssm_parameter: str
+ identity_provider_id_ssm_parameter: str
+ vpc_name_ssm_parameter: str
+
+ @classmethod
+ def from_app_unique_id(cls, app_unique_id: str) -> "ConfigResourceNames":
+ config_prefix = f"/solution/{app_unique_id}/{CMSModuleShortNames.CONFIG}"
+ return ConfigResourceNames(
+ config_prefix=config_prefix,
+ identity_provider_id_ssm_parameter=f"{config_prefix}/auth/identity-provider-id",
+ aws_resource_lookup_lambda_arn_ssm_parameter=f"{config_prefix}/aws-resource-lookup-lambda/arn",
+ vpc_name_ssm_parameter=f"{config_prefix}/vpc/name",
+ )
diff --git a/source/lib/cms_common/resource_names/module_short_names.py b/source/lib/cms_common/resource_names/module_short_names.py
new file mode 100644
index 00000000..1f7d149f
--- /dev/null
+++ b/source/lib/cms_common/resource_names/module_short_names.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from dataclasses import dataclass
+
+# pylint: disable=invalid-name
+
+
+@dataclass(frozen=True)
+class CMSModuleShortNames:
+ ALERTS: str = "alerts"
+ API: str = "api"
+ AUTH: str = "auth"
+ CONFIG: str = "config"
+ CONNECT_STORE: str = "connect-store"
+ EV_BATTERY_HEALTH: str = "ev-battery-health"
+ FLEETWISE_CONNECTOR: str = "fleetwise-connector"
+ PROVISIONING: str = "provisioning"
+ SAMPLE: str = "sample"
+ VEHICLE_SIMULATOR: str = "vehicle-simulator"
+
+
+# pylint: enable=invalid-name
diff --git a/source/lib/cms_common/resource_names/tests/__init__.py b/source/lib/cms_common/resource_names/tests/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/lib/cms_common/resource_names/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/lib/cms_common/resource_names/tests/test_auth.py b/source/lib/cms_common/resource_names/tests/test_auth.py
new file mode 100644
index 00000000..18d383f4
--- /dev/null
+++ b/source/lib/cms_common/resource_names/tests/test_auth.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Third Party Libraries
+import pytest
+
+# Connected Mobility Solution on AWS
+from ..auth import AuthResourceNames
+
+TEST_IDENTITY_PROVIDER_ID = "test-idp"
+
+auth_resource_names = AuthResourceNames.from_identity_provider_id(
+ identity_provider_id=TEST_IDENTITY_PROVIDER_ID
+)
+
+
+@pytest.mark.parametrize(
+ "attribute, expected_attribute_value",
+ [
+ ("auth_prefix", f"/solution/auth/{TEST_IDENTITY_PROVIDER_ID}"),
+ ("idp_config_secret", f"/solution/auth/{TEST_IDENTITY_PROVIDER_ID}/idp-config"),
+ (
+ "idp_config_secret_arn_ssm_parameter",
+ f"/solution/auth/{TEST_IDENTITY_PROVIDER_ID}/idp-config/secret/arn",
+ ),
+ (
+ "client_config_secret",
+ f"/solution/auth/{TEST_IDENTITY_PROVIDER_ID}/client-config/default",
+ ),
+ (
+ "client_config_secret_arn_ssm_parameter",
+ f"/solution/auth/{TEST_IDENTITY_PROVIDER_ID}/client-config/default/secret/arn",
+ ),
+ (
+ "authorization_code_flow_config_secret",
+ f"/solution/auth/{TEST_IDENTITY_PROVIDER_ID}/authorization-code-flow/config",
+ ),
+ (
+ "authorization_code_flow_config_secret_arn_ssm_parameter",
+ f"/solution/auth/{TEST_IDENTITY_PROVIDER_ID}/authorization-code-flow/config/secret/arn",
+ ),
+ ],
+)
+def test_auth(attribute: str, expected_attribute_value: str) -> None:
+ assert getattr(auth_resource_names, attribute) == expected_attribute_value
diff --git a/source/lib/cms_common/resource_names/tests/test_config.py b/source/lib/cms_common/resource_names/tests/test_config.py
new file mode 100644
index 00000000..71adfbf3
--- /dev/null
+++ b/source/lib/cms_common/resource_names/tests/test_config.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Third Party Libraries
+import pytest
+
+# Connected Mobility Solution on AWS
+from ..config import ConfigResourceNames
+
+TEST_APP_UNIQUE_ID = "test-app"
+
+config_resource_names = ConfigResourceNames.from_app_unique_id(TEST_APP_UNIQUE_ID)
+
+
+@pytest.mark.parametrize(
+ "attribute, expected_attribute_value",
+ [
+ ("config_prefix", f"/solution/{TEST_APP_UNIQUE_ID}/config"),
+ (
+ "identity_provider_id_ssm_parameter",
+ f"/solution/{TEST_APP_UNIQUE_ID}/config/auth/identity-provider-id",
+ ),
+ (
+ "aws_resource_lookup_lambda_arn_ssm_parameter",
+ f"/solution/{TEST_APP_UNIQUE_ID}/config/aws-resource-lookup-lambda/arn",
+ ),
+ ],
+)
+def test_config(attribute: str, expected_attribute_value: str) -> None:
+ assert getattr(config_resource_names, attribute) == expected_attribute_value
diff --git a/source/lib/conftest.py b/source/lib/conftest.py
new file mode 100644
index 00000000..065e9ab1
--- /dev/null
+++ b/source/lib/conftest.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import os
+from typing import Dict, Generator
+from unittest.mock import patch
+
+# Third Party Libraries
+import pytest
+
+# isort: off
+# pylint: disable=unused-import
+
+# Connected Mobility Solution on AWS
+from .cms_common.auth.tests.fixture_auth import (
+ fixture_authorization_code_flow_config_secret_string_valid,
+ fixture_client_config_secret_string_valid,
+ fixture_idp_config_secret_string_valid,
+ fixture_mock_client_config_valid,
+ fixture_mock_authorization_code_flow_config_valid,
+ fixture_mock_idp_config_invalid_data_format,
+ fixture_mock_idp_config_invalid_json,
+ fixture_mock_idp_config_valid,
+)
+from .cms_common.boto3_wrappers.tests.fixture_dynamo_crud import (
+ fixture_dynamodb_table,
+ fixture_mock_dynamo_env_vars,
+ fixture_mocked_module_env_vars_values,
+)
+from .cms_common.config.tests.fixture_config import fixture_solution_config
+from .cms_common.constructs.tests.fixture_constructs import (
+ fixture_app_unique_id_cfn_parameter,
+ fixture_app_unique_id_stack,
+ fixture_cdk_lambda_vpc_config_construct_stack_template,
+ fixture_custom_resource_lambda_stack,
+ fixture_identity_provider_config_stack,
+ fixture_empty_lambda_dependencies_stack,
+ fixture_populated_lambda_dependencies_stack,
+ fixture_snapshot_json_with_matcher,
+ fixture_vpc_construct_stack,
+ fixture_vpc_construct_stack_template,
+ fixture_vpc_construct_subnet_selections,
+)
+
+# pylint: enable=unused-import
+# isort: on
+
+# TOP LEVEL SHARED FIXTURES
+# Prevents boto from accidentally using default AWS credentials if not mocked
+@pytest.fixture(name="aws_credentials_env_vars", scope="session")
+def fixture_aws_credentials_env_vars() -> Dict[str, str]:
+ return {
+ "AWS_ACCESS_KEY_ID": "testing", # nosec
+ "AWS_SECRET_ACCESS_ID": "testing", # nosec
+ "AWS_SECURITY_TOKEN": "testing", # nosec
+ "AWS_SESSION_TOKEN": "testing", # nosec
+ "AWS_SECRET_ACCESS_KEY": "testing", # nosec
+ "AWS_DEFAULT_REGION": "us-east-1", # nosec
+ }
+
+
+@pytest.fixture(scope="session", autouse=True)
+def fixture_mock_env_vars(
+ aws_credentials_env_vars: Dict[str, str]
+) -> Generator[None, None, None]:
+ env_vars = {
+ **aws_credentials_env_vars,
+ }
+ with patch.dict(os.environ, env_vars):
+ yield
diff --git a/source/lib/deployment/build-s3-dist.sh b/source/lib/deployment/build-s3-dist.sh
new file mode 100755
index 00000000..d0d2ef48
--- /dev/null
+++ b/source/lib/deployment/build-s3-dist.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+# N/A
+exit
diff --git a/source/lib/deployment/run-cfn-nag.sh b/source/lib/deployment/run-cfn-nag.sh
new file mode 100755
index 00000000..d0d2ef48
--- /dev/null
+++ b/source/lib/deployment/run-cfn-nag.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+# N/A
+exit
diff --git a/source/lib/deployment/run-unit-tests.sh b/source/lib/deployment/run-unit-tests.sh
new file mode 100755
index 00000000..1752a33f
--- /dev/null
+++ b/source/lib/deployment/run-unit-tests.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+
+showHelp() {
+cat << EOF
+Usage: ./deployment/run-unit-tests.sh --help
+
+Run unit tests in this module.
+
+-r, --no-report Don't generate the report, this is mainly used for pre-commit
+
+-s, --snapshot-update Update cdk snapshots
+
+EOF
+}
+
+generate_report=true
+
+for flag in "$@"
+do
+ case "$flag" in
+ -h|--help)
+ showHelp
+ exit 0
+ ;;
+ -r|--no-report)
+ unset generate_report
+ ;;
+ -s|--snapshot-update)
+ snapshot_update=true
+ ;;
+ *)
+ printf "Unrecognized flag %s." "${flag}"
+ printf "Please use --help to see the list of supported flags. This script does not use any positional args.\n"
+ printf "Exiting script with error code 1.\n\n"
+ exit 1
+ ;;
+ esac
+done
+
+cd "$(dirname "$0")"/..
+
+# Get reference for all important folders and files
+project_dir="$PWD"
+source_dir="$project_dir/cms_common"
+python_coverage_report="$source_dir/tests/coverage-reports/coverage.xml"
+python_coverage_report="$source_dir/coverage-reports/coverage.xml"
+
+rm -f "$project_dir/.coverage"
+
+# Run test on package and save results to coverage_report_path in xml format
+pytest "$source_dir" \
+ --cov="$source_dir" \
+ --cov-report=term \
+ --cov-config="$project_dir/pyproject.toml" \
+ ${generate_report:+--cov-report=xml:$python_coverage_report} \
+ ${snapshot_update:+--snapshot-update}
+
+# Only perform the sed transformation if a report was generated, to guarantee the coveragereport file exists
+if [ "$generate_report" = true ]
+then
+ # Linux and MacOS have different ways of calling the sed command for in-place editing.
+ # MacOS takes a mandatory argument for the -i flag whereas linux does not.
+ sedi=(-i)
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sedi=(-i "")
+ fi
+
+ # The pytest coverage report generated includes the absolute path to the root directory.
+ # Sonarqube requires a path that is instead relative to the root directory.
+ # To accomplish this, we remove the absolute path portion of the root directory.
+ repo_root="$(dirname "$(dirname "$project_dir")")"
+ sed "${sedi[@]}" -e "s,$repo_root/,,g" "$python_coverage_report"
+fi
diff --git a/source/lib/deployment/upload-s3-dist.sh b/source/lib/deployment/upload-s3-dist.sh
new file mode 100755
index 00000000..d0d2ef48
--- /dev/null
+++ b/source/lib/deployment/upload-s3-dist.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+# N/A
+exit
diff --git a/source/lib/license_header.txt b/source/lib/license_header.txt
new file mode 100644
index 00000000..03488f70
--- /dev/null
+++ b/source/lib/license_header.txt
@@ -0,0 +1,2 @@
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
diff --git a/source/lib/pyproject.toml b/source/lib/pyproject.toml
new file mode 100644
index 00000000..d042229a
--- /dev/null
+++ b/source/lib/pyproject.toml
@@ -0,0 +1,72 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta:__legacy__"
+
+[tool.coverage.report]
+fail_under = 80.0
+omit = [
+ "**/scripts/*",
+ "setup.py",
+ "**/tests/*",
+ "**/*_dependency_layer/**/*",
+]
+
+[tool.isort]
+sections=["FUTURE", "STDLIB", "THIRDPARTY", "AWS", "FIRSTPARTY", "LOCALFOLDER"]
+known_aws=["aws_cdk","aws_lambda_powertools","aws_solutions_constructs","awscrt","awsiot","cdk_nag","chalice","constructs","boto3","botocore"]
+import_heading_stdlib="Standard Library"
+import_heading_thirdparty="Third Party Libraries"
+import_heading_aws="AWS Libraries"
+import_heading_localfolder="Connected Mobility Solution on AWS"
+profile = "black"
+
+[tool.bandit]
+exclude_dirs = ["cdk.out", "build", ".mypy_cache", ".venv", "*/test_*.py", "*/test_*.py"]
+
+[tool.pylint.'SIMILARITIES']
+ # Ignore comments when computing similarities.
+ignore-comments=true
+ # Ignore docstrings when computing similarities.
+ignore-docstrings=true
+ # Ignore imports when computing similarities.
+ignore-imports=true
+ # Minimum lines number of a similarity.
+min-similarity-lines=15
+
+[tool.pylint.'DESIGN']
+ # Maximum number of arguments for function / method.
+max-args=10
+ # Maximum number of attributes for a class (see R0902).
+max-attributes=12
+ # Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+ # Maximum number of branch for function / method body.
+max-branches=12
+ # Maximum number of locals for function / method body.
+max-locals=15
+ # Maximum number of parents for a class (see R0901).
+max-parents=7
+ # Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+ # Maximum number of return / yield for function / method body.
+max-returns=2
+ # Maximum number of statements in function / method body.
+max-statements=60
+ # Minimum number of public methods for a class (see R0903).
+min-public-methods=0
+
+[tool.pylint.'MESSAGES CONTROL']
+# C0114, C0115, C0116 are for docstrings which we don't use
+# W0511 alarms on leaving TODO, FIXME, etc
+# W0613 alarms on unused arguments
+disable = "C0114, C0115, C0116, W0613, W0511"
+
+[tool.pylint.'FORMAT']
+max-line-length=200
+
+[tool.pylint.'TYPECHECK']
+generated-members=["aws_lambda.Runtime"]
+
+[[tool.mypy.overrides]]
+module = "moto"
+implicit_reexport = true
diff --git a/source/lib/setup.py b/source/lib/setup.py
new file mode 100644
index 00000000..e7e005b8
--- /dev/null
+++ b/source/lib/setup.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import os
+
+# Third Party Libraries
+from setuptools import find_packages, setup
+from setuptools.command.build import build
+from setuptools.command.egg_info import egg_info
+
+
+class CustomDirEggInfo(egg_info):
+ # pylint: disable=attribute-defined-outside-init
+ def initialize_options(self) -> None:
+ egg_info.initialize_options(self)
+
+ self.dist_dir = os.environ.get("MODULE_LIB_DIST_PATH", None)
+ if self.dist_dir is not None:
+ os.makedirs(self.dist_dir, exist_ok=True)
+ self.egg_base = self.dist_dir
+
+ def finalize_options(self) -> None:
+ egg_info.finalize_options(self)
+ self.announce(f"Using directory for egg_info: {self.egg_base}")
+
+
+class CustomDirBuild(build):
+ # pylint: disable=attribute-defined-outside-init
+ def initialize_options(self) -> None:
+ build.initialize_options(self)
+
+ self.dist_dir = os.environ.get("MODULE_LIB_DIST_PATH", None)
+ if self.dist_dir is not None:
+ os.makedirs(self.dist_dir, exist_ok=True)
+ self.build_base = self.dist_dir
+
+ def finalize_options(self) -> None:
+ build.finalize_options(self)
+ self.announce(f"Using directory for build: {self.build_base}")
+
+
+# Explicit setup call necessary for use with `pipenv-setup`
+setup(
+ install_requires=[
+ "aws-lambda-powertools[tracer,validation]>=2.4.0",
+ "cattrs>=22.1.0",
+ "toml>=0.10.2",
+ ],
+ name="cms_common",
+ version="1.0.0",
+ description="Common library used in CMS modules",
+ packages=find_packages(
+ exclude=[
+ "*tests.*",
+ "*tests",
+ ],
+ ),
+ cmdclass={"egg_info": CustomDirEggInfo, "build": CustomDirBuild},
+ package_data={"cms_common": ["py.typed"]},
+ author="AWS Industrial Solutions Team",
+ python_requires=">=3.10",
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: JavaScript",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Typing :: Typed",
+ ],
+)
diff --git a/source/modules/__init__.py b/source/modules/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/.nvmrc b/source/modules/acdp/.nvmrc
new file mode 100644
index 00000000..aacb5181
--- /dev/null
+++ b/source/modules/acdp/.nvmrc
@@ -0,0 +1 @@
+18.17
diff --git a/source/modules/acdp/.pre-commit-config.yaml b/source/modules/acdp/.pre-commit-config.yaml
new file mode 100644
index 00000000..2d17dbf2
--- /dev/null
+++ b/source/modules/acdp/.pre-commit-config.yaml
@@ -0,0 +1,127 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.5.0
+ hooks:
+ - id: name-tests-test
+ name: (ACDP) Check test naming
+ args: ["--pytest-test-first"]
+ exclude: (tests/?.*/fixture(s)?_.*\.py$)
+ - id: check-executables-have-shebangs
+ name: (ACDP) Check executables have shebangs
+ - id: fix-byte-order-marker
+ name: (ACDP) Fix byte order marker
+ - id: check-case-conflict
+ name: (ACDP) Check case conflict
+ - id: check-json
+ name: (ACDP) Check json
+ - id: check-yaml
+ name: (ACDP) Check yaml
+ args: [--allow-multiple-documents, --unsafe]
+ - id: check-toml
+ name: (ACDP) Check toml
+ - id: check-merge-conflict
+ name: (ACDP) Check for merge conflicts
+ - id: check-added-large-files
+ name: (ACDP) Check for added large files
+ exclude: |
+ (?x)^(
+ ^.*/package-lock.json |
+ ^.*/yarn.lock |
+ ^.*/Pipfile.lock
+ )$
+ - id: end-of-file-fixer
+ name: (ACDP) Fix end of files
+ - id: fix-encoding-pragma
+ name: (ACDP) Fix python encoding pragma
+ - id: trailing-whitespace
+ name: (ACDP) Trim trailing whitespace
+ - id: mixed-line-ending
+ name: (ACDP) Mixed line ending
+ - id: detect-aws-credentials
+ name: (ACDP) Detect AWS credentials
+ args: ["--credentials-file", "~/.ada/credentials"]
+ - id: detect-private-key
+ name: (ACDP) Detect private keys
+ - repo: https://github.com/Lucas-C/pre-commit-hooks
+ rev: v1.5.1
+ hooks:
+ - id: insert-license
+ name: (ACDP) Insert license header (python)
+ files: \.py$
+ args:
+ - --license-filepath
+ - ./source/modules/acdp/license_header.txt
+ - --detect-license-in-X-top-lines=3
+ - id: insert-license
+ name: (ACDP) Insert license header (typescript and javascript)
+ files: \.tsx$|\.ts$|\.js$|\.jsx$
+ args:
+ - --license-filepath
+ - ./source/modules/acdp/license_header.txt
+ - --comment-style
+ - // # defaults to Python's # syntax, requires changing for typescript syntax.
+ - --detect-license-in-X-top-lines=3
+ - repo: https://github.com/psf/black
+ rev: 22.3.0
+ hooks:
+ - id: black
+ name: (ACDP) Black
+ - repo: https://github.com/hadialqattan/pycln
+ rev: v2.1.3
+ hooks:
+ - id: pycln
+ name: (ACDP) Pycln
+ - repo: https://github.com/pycqa/isort
+ rev: 5.12.0
+ hooks:
+ - id: isort
+ name: (ACDP) Isort (python)
+ args: ["--skip-glob", "**/node_modules/* **/.venv/*", "--settings-path", "./source/modules/acdp/pyproject.toml"]
+ - repo: https://github.com/PyCQA/bandit
+ rev: 1.7.4
+ hooks:
+ - id: bandit
+ name: (ACDP) Bandit
+ args: ["-c", "./source/modules/acdp/pyproject.toml"]
+ additional_dependencies: [ "bandit[toml]" ]
+ - repo: https://github.com/pypa/pip-audit
+ rev: v2.6.1
+ hooks:
+ - id: pip-audit
+ name: (ACDP) Pip audit
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v3.1.0
+ hooks:
+ - id: prettier
+ name: (ACDP) Prettier
+ types_or: [javascript, jsx, ts, tsx]
+ # Local
+ - repo: local
+ hooks:
+ - id: shellcheck
+ name: (ACDP) Shellchecker
+ entry: shellcheck
+ args: ["-x"]
+ types: [shell]
+ language: system
+ - id: pylint
+ name: (ACDP) pylint
+ entry: pylint
+ args: ["--extension-pkg-allow-list", "math", "--rcfile", "./source/modules/acdp/pyproject.toml"]
+ types: [python]
+ language: system
+ - id: mypy
+ name: (ACDP) mypy
+ entry: mypy
+ types_or: [python, pyi]
+ args: ["--strict", "--cache-dir", "./source/modules/acdp/.mypy_cache", "--config-file", "./source/modules/acdp/pyproject.toml"]
+ language: system
+ - id: run-tsc-backstage
+ name: (ACDP) Run tsc on Backstage
+ entry: source/modules/acdp/deployment/run-backstage-lint.sh
+ language: system
+ types_or: [ts, tsx]
+ pass_filenames: false
diff --git a/source/modules/acdp/.python-version b/source/modules/acdp/.python-version
new file mode 100644
index 00000000..c8cfe395
--- /dev/null
+++ b/source/modules/acdp/.python-version
@@ -0,0 +1 @@
+3.10
diff --git a/source/modules/acdp/Makefile b/source/modules/acdp/Makefile
new file mode 100644
index 00000000..8d708258
--- /dev/null
+++ b/source/modules/acdp/Makefile
@@ -0,0 +1,129 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+.DEFAULT_GOAL := help
+
+# ========================================================
+# SOLUTION METADATA
+# ========================================================
+export MODULE_NAME ?= acdp
+export MODULE_SHORT_NAME ?= ${MODULE_NAME}
+export MODULE_VERSION ?= ${SOLUTION_VERSION}
+export MODULE_DESCRIPTION ?= Deployment solution using Spotify Backstage to deploy and manage CMS modules
+export MODULE_AUTHOR ?= AWS Industrial Solutions Team
+
+SOLUTION_PATH := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../../..)
+MODULE_PATH := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
+
+# ========================================================
+# VARIABLES
+# ========================================================
+export ACDP_UNIQUE_ID ?= acdp
+
+export STACK_NAME ?= ${ACDP_UNIQUE_ID}--${MODULE_NAME}
+export STACK_TEMPLATE_NAME = ${MODULE_NAME}.template
+export STACK_TEMPLATE_PATH ?= deployment/global-s3-assets/${MODULE_NAME}/${STACK_TEMPLATE_NAME}
+
+export CAPABILITY_ID ?= CMS.6
+
+export BACKSTAGE_ASSETS_PREFIX ?= ${SOLUTION_NAME}/${SOLUTION_VERSION}/backstage
+export BACKSTAGE_LOG_LEVEL ?= info
+export BACKSTAGE_S3_DISCOVERY_REFRESH_MINS ?= 30
+export BACKSTAGE_NAME ?= DEFAULT_NAME
+export BACKSTAGE_ORG ?= DEFAULT_ORG
+
+export ROUTE53_BASE_DOMAIN ?= ${ROUTE53_ZONE_NAME}
+
+include ${SOLUTION_PATH}/makefiles/common_config.mk
+include ${SOLUTION_PATH}/makefiles/global_targets.mk
+include ${SOLUTION_PATH}/makefiles/module_targets.mk
+
+## ========================================================
+## INSTALL
+## ========================================================
+.PHONY: yarn-install
+yarn-install: ## Using yarn, installs node dependencies for all modules.
+ @printf "%bInstalling node dependencies using yarn.%b\n" "${MAGENTA}" "${NC}"
+ cd backstage && yarn install
+
+.PHONY: install
+install: pipenv-install yarn-install ## Installs the resources and dependencies required to build the solution.
+ @printf "%bInstall finished.%b\n" "${GREEN}" "${NC}"
+
+## ========================================================
+## BUILD AND DEPLOY
+## ========================================================
+.PHONY: deploy
+deploy: verify-environment ## Deploy the stack for the module.
+ @printf "%bDeploy the module.%b\n" "${MAGENTA}" "${NC}"
+ aws cloudformation deploy \
+ --stack-name ${STACK_NAME} \
+ --template-file ${STACK_TEMPLATE_PATH} \
+ --s3-bucket ${GLOBAL_ASSET_BUCKET_NAME} \
+ --s3-prefix ${SOLUTION_NAME}/local/${MODULE_NAME} \
+ --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \
+ --parameter-overrides \
+ "AcdpUniqueId"="${ACDP_UNIQUE_ID}" \
+ "UserEmail"="${USER_EMAIL}" \
+ "Route53ZoneName"="${ROUTE53_ZONE_NAME}" \
+ "Route53BaseDomain"="${ROUTE53_BASE_DOMAIN}" \
+ "BackstageName"="${BACKSTAGE_NAME}" \
+ "BackstageOrg"="${BACKSTAGE_ORG}" \
+ "BackstageLogLevel"="${BACKSTAGE_LOG_LEVEL}" \
+ "BackstageLocalAssetDiscoveryRefreshMins"="${BACKSTAGE_S3_DISCOVERY_REFRESH_MINS}" \
+ "VpcName"="${VPC_NAME}" \
+
+.PHONY: destroy-ecr
+destroy-ecr: ## Destroy the ECR images since CloudFormation cannot.
+ @printf "%bDelete the ECR repository.%b\n" "${MAGENTA}" "${NC}"
+ aws ecr delete-repository --repository-name "${ACDP_UNIQUE_ID}-backstage" --force || true
+
+.PHONY: destroy
+destroy: destroy-ecr destroy-stack ## Delete the stack for the module.
+ @printf "%bDelete the module deployment.%b\n" "${MAGENTA}" "${NC}"
+ aws ecr delete-repository --repository-name "${ACDP_UNIQUE_ID}-backstage" --force || true
+ aws cloudformation delete-stack \
+ --stack-name ${STACK_NAME} \
+
+## ========================================================
+## LOCAL UTILITY
+## ========================================================
+
+.PHONY: run-backstage-local
+run-backstage-local: run-postgres-local ## Start a local instance of Backstage
+ cd backstage && yarn run dev
+
+.PHONY: run-backstage-backend-local
+run-backstage-backend-local: run-postgres-local ## Start a local instance of Backstage's backend
+ cd backstage && yarn run start-backend
+
+.PHONY: run-backstage-frontend-local
+run-backstage-frontend-local: ## Start a local instance of Backstage's frontend
+ cd backstage && yarn start
+
+.PHONY: run-postgres-local
+run-postgres-local: ## Start a local instance of postgres for use with Backstage
+ cd backstage && docker-compose up &> ./docker_postgres.log &
+
+.PHONY: stop-backstage-local
+stop-backstage-local:
+ cd backstage && docker-compose stop && rm -f ./docker_postgres.log
+
+## ========================================================
+## UTILITY
+## ========================================================
+.PHONY: verify-environment
+verify-environment: ## Checks the cdk environment for the required environment variables.
+ifneq (, $(wildcard ./cdk.context.json))
+ $(error 'cdk.context.json' cannot exist. Please delete the file and try again)
+endif
+ifndef VPC_NAME
+ $(error VPC_NAME is undefined. Set the variable using `export VPC_NAME=...`, or run `source .cmsrc`)
+endif
+ifndef USER_EMAIL
+ $(error USER_EMAIL is undefined. Set the variable using `export USER_EMAIL=...`, or run `source .cmsrc`)
+endif
+ifndef ROUTE53_ZONE_NAME
+ $(error ROUTE53_ZONE_NAME is undefined. Set the variable using `export ROUTE53_ZONE_NAME=...`, or run `source .cmsrc`)
+endif
+ @printf "%bEnvironment variables verified.%b\n" "${GREEN}" "${NC}"
diff --git a/source/modules/acdp/Pipfile b/source/modules/acdp/Pipfile
new file mode 100644
index 00000000..5a6bbe58
--- /dev/null
+++ b/source/modules/acdp/Pipfile
@@ -0,0 +1,40 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+aws_lambda_powertools = {extras=["tracer", "validation"], version=">=2.4.0"}
+requests = ">=2.28.1"
+
+[dev-packages]
+cms_common = {path = "./../../lib", editable = true}
+attrs = ">=22.1.0"
+aws-cdk-lib = ">=2.63.2"
+boto3 = ">=1.26.0"
+boto3-stubs = {extras = ["essential"], version = "*"}
+cdk-nag = "*"
+jinja2 = "*"
+markdown-to-json = "*"
+mypy = "*"
+pre-commit = "*"
+pycln = "*"
+pylint = "*"
+pytest = "*"
+pytest-cov = "*"
+pytest-mock = "*"
+syrupy = "*"
+toml = "*"
+types-boto3 = ">=1.0.2"
+types-python-dateutil = "*"
+types-pyyaml = "*"
+types-requests = ">=2.28.1"
+types-setuptools = "*"
+types-urllib3 = "*"
+types-toml = "*"
+wheel = "*"
+wrapt = "*"
+freezegun="*"
+
+[requires]
+python_version = "3.10"
diff --git a/source/modules/acdp/Pipfile.lock b/source/modules/acdp/Pipfile.lock
new file mode 100644
index 00000000..a38c07ed
--- /dev/null
+++ b/source/modules/acdp/Pipfile.lock
@@ -0,0 +1,1244 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "460a367e697598be082ad9b24ea782cb72a741617f4f32789c37f4b835a19842"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.10"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "aws-lambda-powertools": {
+ "extras": [
+ "tracer",
+ "validation"
+ ],
+ "hashes": [
+ "sha256:02fafbdaaa0a89faaf8c49777a22996aaba465a099d69b4f1fbfd8c3ae47fc41",
+ "sha256:cfcc41d7125b9527b8fd8c4e3e4b30b971f915c32ec0c6e39573fd9f298a63a8"
+ ],
+ "markers": "python_version >= '3.8' and python_full_version < '4.0.0'",
+ "version": "==2.34.2"
+ },
+ "aws-xray-sdk": {
+ "hashes": [
+ "sha256:0bbfdbc773cfef4061062ac940b85e408297a2242f120bcdfee2593209b1e432",
+ "sha256:f6803832dc08d18cc265e2327a69bfa9ee41c121fac195edc9745d04b7a566c3"
+ ],
+ "version": "==2.12.1"
+ },
+ "botocore": {
+ "hashes": [
+ "sha256:01d5156247f991b3466a8404e3d7460a9ecbd9b214f9992d6ba797d9ddc6f120",
+ "sha256:5086217442e67dd9de36ec7e87a0c663f76b7790d5fb6a12de565af95e87e319"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "certifi": {
+ "hashes": [
+ "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
+ "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==2024.2.2"
+ },
+ "charset-normalizer": {
+ "hashes": [
+ "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027",
+ "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087",
+ "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786",
+ "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8",
+ "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09",
+ "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185",
+ "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574",
+ "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e",
+ "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519",
+ "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898",
+ "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269",
+ "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3",
+ "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f",
+ "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6",
+ "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8",
+ "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a",
+ "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73",
+ "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc",
+ "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714",
+ "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2",
+ "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc",
+ "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce",
+ "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d",
+ "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e",
+ "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6",
+ "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269",
+ "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96",
+ "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d",
+ "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a",
+ "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4",
+ "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77",
+ "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d",
+ "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0",
+ "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed",
+ "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068",
+ "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac",
+ "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25",
+ "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8",
+ "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab",
+ "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26",
+ "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2",
+ "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db",
+ "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f",
+ "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5",
+ "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99",
+ "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c",
+ "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d",
+ "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811",
+ "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa",
+ "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a",
+ "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03",
+ "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b",
+ "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04",
+ "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c",
+ "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001",
+ "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458",
+ "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389",
+ "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99",
+ "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985",
+ "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537",
+ "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238",
+ "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f",
+ "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d",
+ "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796",
+ "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a",
+ "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143",
+ "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8",
+ "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c",
+ "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5",
+ "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5",
+ "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711",
+ "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4",
+ "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6",
+ "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c",
+ "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7",
+ "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4",
+ "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b",
+ "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae",
+ "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12",
+ "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c",
+ "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae",
+ "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8",
+ "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887",
+ "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b",
+ "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4",
+ "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f",
+ "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5",
+ "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33",
+ "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519",
+ "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==3.3.2"
+ },
+ "fastjsonschema": {
+ "hashes": [
+ "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0",
+ "sha256:e3126a94bdc4623d3de4485f8d468a12f02a67921315ddc87836d6e456dc789d"
+ ],
+ "version": "==2.19.1"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
+ "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==3.6"
+ },
+ "jmespath": {
+ "hashes": [
+ "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
+ "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.0.1"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==2.8.2"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
+ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==2.31.0"
+ },
+ "six": {
+ "hashes": [
+ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+ "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==1.16.0"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
+ "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.10.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
+ "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.7"
+ },
+ "wrapt": {
+ "hashes": [
+ "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc",
+ "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81",
+ "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09",
+ "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e",
+ "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca",
+ "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0",
+ "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb",
+ "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487",
+ "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40",
+ "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c",
+ "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060",
+ "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202",
+ "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41",
+ "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9",
+ "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b",
+ "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664",
+ "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d",
+ "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362",
+ "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00",
+ "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc",
+ "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1",
+ "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267",
+ "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956",
+ "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966",
+ "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1",
+ "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228",
+ "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72",
+ "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d",
+ "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292",
+ "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0",
+ "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0",
+ "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36",
+ "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c",
+ "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5",
+ "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f",
+ "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73",
+ "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b",
+ "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2",
+ "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593",
+ "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39",
+ "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389",
+ "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf",
+ "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf",
+ "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89",
+ "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c",
+ "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c",
+ "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f",
+ "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440",
+ "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465",
+ "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136",
+ "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b",
+ "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8",
+ "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3",
+ "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8",
+ "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6",
+ "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e",
+ "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f",
+ "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c",
+ "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e",
+ "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8",
+ "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2",
+ "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020",
+ "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35",
+ "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d",
+ "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3",
+ "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537",
+ "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809",
+ "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d",
+ "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a",
+ "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==1.16.0"
+ }
+ },
+ "develop": {
+ "astroid": {
+ "hashes": [
+ "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819",
+ "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"
+ ],
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==3.1.0"
+ },
+ "attrs": {
+ "hashes": [
+ "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30",
+ "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==23.2.0"
+ },
+ "aws-cdk-lib": {
+ "hashes": [
+ "sha256:03a98770dd58caa002ded8d2dcdd3f6f7451a95f86c8dba3b5f2b70e659429b3",
+ "sha256:b9ed68a5fd7f5b9056da58bd122c9c3faa6af1e92f4b6aff181a2ee57625aad1"
+ ],
+ "index": "pypi",
+ "markers": "python_version ~= '3.8'",
+ "version": "==2.130.0"
+ },
+ "aws-cdk.asset-awscli-v1": {
+ "hashes": [
+ "sha256:3ef87d6530736b3a7b0f777fe3b4297994dd40c3ce9306d95f80f48fb18036e8",
+ "sha256:96205ea2e5e132ec52fabfff37ea25b9b859498f167d05b32564c949822cd331"
+ ],
+ "markers": "python_version ~= '3.8'",
+ "version": "==2.2.202"
+ },
+ "aws-cdk.asset-kubectl-v20": {
+ "hashes": [
+ "sha256:346283e43018a43e3b3ca571de3f44e85d49c038dc20851894cb8f9b2052b164",
+ "sha256:7f0617ab6cb942b066bd7174bf3e1f377e57878c3e1cddc21d6b2d13c92d0cc1"
+ ],
+ "markers": "python_version ~= '3.7'",
+ "version": "==2.1.2"
+ },
+ "aws-cdk.asset-node-proxy-agent-v6": {
+ "hashes": [
+ "sha256:42cdbc1de2ed3f845e3eb883a72f58fc7e5554c2e0b6fcdb366c159778dce74d",
+ "sha256:e442673d4f93137ab165b75386761b1d46eea25fc5015e5145ae3afa9da06b6e"
+ ],
+ "markers": "python_version ~= '3.7'",
+ "version": "==2.0.1"
+ },
+ "aws-lambda-powertools": {
+ "extras": [
+ "tracer",
+ "validation"
+ ],
+ "hashes": [
+ "sha256:02fafbdaaa0a89faaf8c49777a22996aaba465a099d69b4f1fbfd8c3ae47fc41",
+ "sha256:cfcc41d7125b9527b8fd8c4e3e4b30b971f915c32ec0c6e39573fd9f298a63a8"
+ ],
+ "markers": "python_version >= '3.8' and python_full_version < '4.0.0'",
+ "version": "==2.34.2"
+ },
+ "aws-xray-sdk": {
+ "hashes": [
+ "sha256:0bbfdbc773cfef4061062ac940b85e408297a2242f120bcdfee2593209b1e432",
+ "sha256:f6803832dc08d18cc265e2327a69bfa9ee41c121fac195edc9745d04b7a566c3"
+ ],
+ "version": "==2.12.1"
+ },
+ "boto3": {
+ "hashes": [
+ "sha256:2cd9463e738a184cbce8a6824027c22163c5f73e277a35ff5aa0fb0e845b4301",
+ "sha256:67732634dc7d0afda879bd9a5e2d0818a2c14a98bef766b95a3e253ea5104cb9"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "boto3-stubs": {
+ "extras": [
+ "essential"
+ ],
+ "hashes": [
+ "sha256:3c3283d3982099cfbe6fee474f8eae42217b7cdfd98d5dd857ea952e29bdabf1",
+ "sha256:c04ece156a376745af34aefe7283e93f7066d8f2be2500297b129e3d46e0ac26"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "botocore": {
+ "hashes": [
+ "sha256:01d5156247f991b3466a8404e3d7460a9ecbd9b214f9992d6ba797d9ddc6f120",
+ "sha256:5086217442e67dd9de36ec7e87a0c663f76b7790d5fb6a12de565af95e87e319"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "botocore-stubs": {
+ "hashes": [
+ "sha256:8748b9fe01f66bb1e7b13f45e3336e2e2c5460d232816d45941573425459c66e",
+ "sha256:d0f4d9859d9f6affbe4b0b46e37fe729860eaab55ebefe7e09cf567396b2feda"
+ ],
+ "markers": "python_version >= '3.8' and python_version < '4.0'",
+ "version": "==1.34.51"
+ },
+ "cattrs": {
+ "hashes": [
+ "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108",
+ "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==23.2.3"
+ },
+ "cdk-nag": {
+ "hashes": [
+ "sha256:602d8a91252424f557f2dc991dca413dbdd7ae656303d961a849634a4181532a",
+ "sha256:8f62603886eac9072aa77fc79700efdc6d1ac44a7b8537516f8adf849d59dae9"
+ ],
+ "index": "pypi",
+ "markers": "python_version ~= '3.8'",
+ "version": "==2.28.48"
+ },
+ "cfgv": {
+ "hashes": [
+ "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9",
+ "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.4.0"
+ },
+ "click": {
+ "hashes": [
+ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+ "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==8.1.7"
+ },
+ "cms-common": {
+ "editable": true,
+ "path": "./../../lib"
+ },
+ "constructs": {
+ "hashes": [
+ "sha256:2972f514837565ff5b09171cfba50c0159dfa75ee86a42921ea8c86f2941b3d2",
+ "sha256:518551135ec236f9cc6b86500f4fbbe83b803ccdc6c2cb7684e0b7c4d234e7b1"
+ ],
+ "markers": "python_version ~= '3.7'",
+ "version": "==10.3.0"
+ },
+ "coverage": {
+ "extras": [
+ "toml"
+ ],
+ "hashes": [
+ "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa",
+ "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003",
+ "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f",
+ "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c",
+ "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e",
+ "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0",
+ "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9",
+ "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52",
+ "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e",
+ "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454",
+ "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0",
+ "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079",
+ "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352",
+ "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f",
+ "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30",
+ "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe",
+ "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113",
+ "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765",
+ "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc",
+ "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e",
+ "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501",
+ "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7",
+ "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2",
+ "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f",
+ "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4",
+ "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524",
+ "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c",
+ "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51",
+ "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840",
+ "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6",
+ "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee",
+ "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e",
+ "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45",
+ "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba",
+ "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d",
+ "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3",
+ "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10",
+ "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e",
+ "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb",
+ "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9",
+ "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a",
+ "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47",
+ "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1",
+ "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3",
+ "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914",
+ "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328",
+ "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6",
+ "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d",
+ "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0",
+ "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94",
+ "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc",
+ "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==7.4.3"
+ },
+ "dill": {
+ "hashes": [
+ "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca",
+ "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==0.3.8"
+ },
+ "distlib": {
+ "hashes": [
+ "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784",
+ "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"
+ ],
+ "version": "==0.3.8"
+ },
+ "exceptiongroup": {
+ "hashes": [
+ "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14",
+ "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==1.2.0"
+ },
+ "fastjsonschema": {
+ "hashes": [
+ "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0",
+ "sha256:e3126a94bdc4623d3de4485f8d468a12f02a67921315ddc87836d6e456dc789d"
+ ],
+ "version": "==2.19.1"
+ },
+ "filelock": {
+ "hashes": [
+ "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e",
+ "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.13.1"
+ },
+ "freezegun": {
+ "hashes": [
+ "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b",
+ "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==1.4.0"
+ },
+ "identify": {
+ "hashes": [
+ "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791",
+ "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==2.5.35"
+ },
+ "importlib-resources": {
+ "hashes": [
+ "sha256:308abf8474e2dba5f867d279237cd4076482c3de7104a40b41426370e891549b",
+ "sha256:9a0a862501dc38b68adebc82970140c9e4209fc99601782925178f8386339938"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==6.1.2"
+ },
+ "iniconfig": {
+ "hashes": [
+ "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
+ "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.0"
+ },
+ "isort": {
+ "hashes": [
+ "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109",
+ "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"
+ ],
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==5.13.2"
+ },
+ "jinja2": {
+ "hashes": [
+ "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
+ "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==3.1.3"
+ },
+ "jmespath": {
+ "hashes": [
+ "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
+ "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.0.1"
+ },
+ "jsii": {
+ "hashes": [
+ "sha256:1105bae271ae47c27cf31c1565c5157306efed5ad9323c9a27336f962f465716",
+ "sha256:175abc356603d98f18ab6f6aa74bfeae253e4e56340aef9dc40bbb1a6a59868b"
+ ],
+ "markers": "python_version ~= '3.8'",
+ "version": "==1.94.0"
+ },
+ "libcst": {
+ "hashes": [
+ "sha256:0cb92398236566f0b73a0c73f8a41a9c4906c793e8f7c2745f30e3fb141a34b5",
+ "sha256:13ca9fe82326d82feb2c7b0f5a320ce7ed0d707c32919dd36e1f40792459bf6f",
+ "sha256:1b5fecb2b26fa3c1efe6e05ef1420522bd31bb4dae239e4c41fdf3ddbd853aeb",
+ "sha256:1d45718f7e7a1405a16fd8e7fc75c365120001b6928bfa3c4112f7e533990b9a",
+ "sha256:2bbb4e442224da46b59a248d7d632ed335eae023a921dea1f5c72d2a059f6be9",
+ "sha256:38fbd56f885e1f77383a6d1d798a917ffbc6d28dc6b1271eddbf8511c194213e",
+ "sha256:3c7c0edfe3b878d64877671261c7b3ffe9d23181774bfad5d8fcbdbbbde9f064",
+ "sha256:4973a9d509cf1a59e07fac55a98f70bc4fd35e09781dffb3ec93ee32fc0de7af",
+ "sha256:5c0d548d92c6704bb07ce35d78c0e054cdff365def0645c1b57c856c8e112bb4",
+ "sha256:5e54389abdea995b39ee96ad736ed1b0b8402ed30a7956b7a279c10baf0c0294",
+ "sha256:6dd388c74c04434b41e3b25fc4a0fafa3e6abf91f97181df55e8f8327fd903cc",
+ "sha256:71dd69fff76e7edaf8fae0f63ffcdbf5016e8cd83165b1d0688d6856aa48186a",
+ "sha256:7f4919978c2b395079b64d8a654357854767adbabab13998b39c1f0bc67da8a7",
+ "sha256:82373a35711a8bb2a664dba2b7aeb20bbcce92a4db40af964e9cb2b976f989e7",
+ "sha256:8b56130f18aca9a98b3bcaf5962b2b26c2dcdd6d5132decf3f0b0b635f4403ba",
+ "sha256:968b93400e66e6711a29793291365e312d206dbafd3fc80219cfa717f0f01ad5",
+ "sha256:b4066dcadf92b183706f81ae0b4342e7624fc1d9c5ca2bf2b44066cb74bf863f",
+ "sha256:ba24b8cf789db6b87c6e23a6c6365f5f73cb7306d929397581d5680149e9990c",
+ "sha256:c0149d24a455536ff2e41b3a48b16d3ebb245e28035013c91bd868def16592a0",
+ "sha256:c80f36f4a02d530e28eac7073aabdea7c6795fc820773a02224021d79d164e8b",
+ "sha256:dded0e4f2e18150c4b07fedd7ef84a9abc7f9bd2d47cc1c485248ee1ec58e5cc",
+ "sha256:dece0362540abfc39cd2cf5c98cde238b35fd74a1b0167e2563e4b8bb5f47489",
+ "sha256:e01879aa8cd478bb8b1e4285cfd0607e64047116f7ab52bc2a787cde584cd686",
+ "sha256:f080e9af843ff609f8f35fc7275c8bf08b02c31115e7cd5b77ca3b6a56c75096",
+ "sha256:f2342634f6c61fc9076dc0baf21e9cf5ef0195a06e1e95c0c9dc583ba3a30d00"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==1.2.0"
+ },
+ "markdown-to-json": {
+ "hashes": [
+ "sha256:44a17e3ff42af4f049fa2a6a86efbe30e27dcf8401c7ad1772b97b2d396d88f8",
+ "sha256:ea02313f7c5e8d05033d7a2b4e7c891246bc8f6391e1681760579687e6b0ba68"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.6.2'",
+ "version": "==2.1.0"
+ },
+ "markupsafe": {
+ "hashes": [
+ "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
+ "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
+ "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
+ "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
+ "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
+ "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
+ "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
+ "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
+ "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
+ "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
+ "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
+ "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
+ "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
+ "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
+ "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
+ "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
+ "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
+ "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
+ "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
+ "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
+ "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
+ "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
+ "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
+ "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
+ "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
+ "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
+ "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
+ "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
+ "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
+ "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
+ "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
+ "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
+ "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
+ "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
+ "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
+ "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
+ "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
+ "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
+ "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
+ "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
+ "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
+ "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
+ "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
+ "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
+ "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
+ "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
+ "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
+ "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
+ "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
+ "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
+ "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
+ "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
+ "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
+ "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
+ "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
+ "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
+ "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
+ "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
+ "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
+ "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.1.5"
+ },
+ "mccabe": {
+ "hashes": [
+ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
+ "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.7.0"
+ },
+ "mypy": {
+ "hashes": [
+ "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6",
+ "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d",
+ "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02",
+ "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d",
+ "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3",
+ "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3",
+ "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3",
+ "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66",
+ "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259",
+ "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835",
+ "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd",
+ "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d",
+ "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8",
+ "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07",
+ "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b",
+ "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e",
+ "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6",
+ "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae",
+ "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9",
+ "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d",
+ "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a",
+ "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592",
+ "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218",
+ "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817",
+ "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4",
+ "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410",
+ "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==1.8.0"
+ },
+ "mypy-boto3-cloudformation": {
+ "hashes": [
+ "sha256:49d04c090dae3fd8289738ae592cac9d6faa5169684de40c2730b425bba2a32d",
+ "sha256:bfe5ec405eae6dae31dc9874729eef5e668e634eae8972032f00400d17bd2c7d"
+ ],
+ "version": "==1.34.32"
+ },
+ "mypy-boto3-dynamodb": {
+ "hashes": [
+ "sha256:126da0a29ca48502cfa9a26e3024341233d8419f7e03273cea17af7d38e724bd",
+ "sha256:1af7c80a0891edac29e5b70441122f6803eb772a3b7b498396eec30368232541"
+ ],
+ "version": "==1.34.46"
+ },
+ "mypy-boto3-ec2": {
+ "hashes": [
+ "sha256:702378c68af01c47c1fd6e739f16599b0c388045127a993e0cc41dbbff31cc0d",
+ "sha256:ea74f5a45f1c4bfa8c21604ab391d3c504b218c2db091488d7c803bd9b443c9c"
+ ],
+ "version": "==1.34.50"
+ },
+ "mypy-boto3-lambda": {
+ "hashes": [
+ "sha256:275297944c5e36a170b37ce70229f21db6dd3561606799f18d96e36ac5df6876",
+ "sha256:a12232002e04ee06b413b47068bc6bb085aeaa3693d28e9bf0efd76fa6953a0b"
+ ],
+ "version": "==1.34.46"
+ },
+ "mypy-boto3-rds": {
+ "hashes": [
+ "sha256:59124bd98653c73c685b7dc0d0a9069572d340f0ecb116a9706aa3e2d40a166d",
+ "sha256:9561dfac562ec9cd039806d5de2bc2bb8be4f9f7c03620270550a49e456fef46"
+ ],
+ "version": "==1.34.50"
+ },
+ "mypy-boto3-s3": {
+ "hashes": [
+ "sha256:71c39ab0623cdb442d225b71c1783f6a513cff4c4a13505a2efbb2e3aff2e965",
+ "sha256:f9669ecd182d5bf3532f5f2dcc5e5237776afe157ad5a0b37b26d6bec5fcc432"
+ ],
+ "version": "==1.34.14"
+ },
+ "mypy-boto3-sqs": {
+ "hashes": [
+ "sha256:0bf8995f58919ab295398100e72eaa7da898adcfd9d339a42f3c48ce473419d5",
+ "sha256:94d8aea4ae75605f70e58e440d706e04d5c614101ddb2f0c73d306d776d10995"
+ ],
+ "version": "==1.34.0"
+ },
+ "mypy-extensions": {
+ "hashes": [
+ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
+ "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==1.0.0"
+ },
+ "nodeenv": {
+ "hashes": [
+ "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2",
+ "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
+ "version": "==1.8.0"
+ },
+ "packaging": {
+ "hashes": [
+ "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
+ "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==23.2"
+ },
+ "pathspec": {
+ "hashes": [
+ "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
+ "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.12.1"
+ },
+ "platformdirs": {
+ "hashes": [
+ "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068",
+ "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.2.0"
+ },
+ "pluggy": {
+ "hashes": [
+ "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981",
+ "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.4.0"
+ },
+ "pre-commit": {
+ "hashes": [
+ "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c",
+ "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==3.6.2"
+ },
+ "publication": {
+ "hashes": [
+ "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6",
+ "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"
+ ],
+ "version": "==0.0.3"
+ },
+ "pycln": {
+ "hashes": [
+ "sha256:1f3eefb7be18a9ee06c3bdd0ba2e91218cd39317e20130325f107e96eb84b9f6",
+ "sha256:d1bf648df17077306100815d255d45430035b36f66bac635df04a323c61ba126"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.7.0' and python_version < '4'",
+ "version": "==2.4.0"
+ },
+ "pylint": {
+ "hashes": [
+ "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74",
+ "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==3.1.0"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd",
+ "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==8.0.2"
+ },
+ "pytest-cov": {
+ "hashes": [
+ "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6",
+ "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==4.1.0"
+ },
+ "pytest-mock": {
+ "hashes": [
+ "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f",
+ "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==3.12.0"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==2.8.2"
+ },
+ "pyyaml": {
+ "hashes": [
+ "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
+ "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+ "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
+ "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+ "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+ "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+ "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+ "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+ "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+ "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+ "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
+ "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
+ "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+ "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
+ "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+ "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+ "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+ "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+ "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+ "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+ "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+ "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
+ "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+ "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+ "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+ "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
+ "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
+ "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+ "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+ "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
+ "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+ "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+ "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+ "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+ "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+ "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+ "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+ "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+ "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+ "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+ "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+ "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+ "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
+ "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+ "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
+ "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+ "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+ "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+ "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+ "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+ "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==6.0.1"
+ },
+ "s3transfer": {
+ "hashes": [
+ "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e",
+ "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.10.0"
+ },
+ "setuptools": {
+ "hashes": [
+ "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56",
+ "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==69.1.1"
+ },
+ "six": {
+ "hashes": [
+ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+ "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==1.16.0"
+ },
+ "syrupy": {
+ "hashes": [
+ "sha256:203e52f9cb9fa749cf683f29bd68f02c16c3bc7e7e5fe8f2fc59bdfe488ce133",
+ "sha256:37a835c9ce7857eeef86d62145885e10b3cb9615bc6abeb4ce404b3f18e1bb36"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.8.1' and python_version < '4'",
+ "version": "==4.6.1"
+ },
+ "toml": {
+ "hashes": [
+ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+ "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==0.10.2"
+ },
+ "tomli": {
+ "hashes": [
+ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
+ "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==2.0.1"
+ },
+ "tomlkit": {
+ "hashes": [
+ "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b",
+ "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==0.12.4"
+ },
+ "typeguard": {
+ "hashes": [
+ "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4",
+ "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"
+ ],
+ "markers": "python_full_version >= '3.5.3'",
+ "version": "==2.13.3"
+ },
+ "typer": {
+ "hashes": [
+ "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2",
+ "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.9.0"
+ },
+ "types-awscrt": {
+ "hashes": [
+ "sha256:10245570c7285e949362b4ae710c54bf285d64a27453d42762477bcee5cd77a3",
+ "sha256:73be0a2720d6f76b924df6917d4edf4c9958f83e5c25bf7d9f0c1e9cdf836941"
+ ],
+ "markers": "python_version >= '3.7' and python_version < '4.0'",
+ "version": "==0.20.4"
+ },
+ "types-boto3": {
+ "hashes": [
+ "sha256:15f3ffad0314e40a0708fec25f94891414f93260202422bf8b19b6913853c983",
+ "sha256:a6a88e94d59d887839863a64095493956efc148e747206880a7eb47d90ae8398"
+ ],
+ "index": "pypi",
+ "version": "==1.0.2"
+ },
+ "types-python-dateutil": {
+ "hashes": [
+ "sha256:1f8db221c3b98e6ca02ea83a58371b22c374f42ae5bbdf186db9c9a76581459f",
+ "sha256:efbbdc54590d0f16152fa103c9879c7d4a00e82078f6e2cf01769042165acaa2"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==2.8.19.20240106"
+ },
+ "types-pyyaml": {
+ "hashes": [
+ "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062",
+ "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"
+ ],
+ "index": "pypi",
+ "version": "==6.0.12.12"
+ },
+ "types-requests": {
+ "hashes": [
+ "sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b",
+ "sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==2.31.0.20240218"
+ },
+ "types-s3transfer": {
+ "hashes": [
+ "sha256:35e4998c25df7f8985ad69dedc8e4860e8af3b43b7615e940d53c00d413bdc69",
+ "sha256:44fcdf0097b924a9aab1ee4baa1179081a9559ca62a88c807e2b256893ce688f"
+ ],
+ "markers": "python_version >= '3.7' and python_version < '4.0'",
+ "version": "==0.10.0"
+ },
+ "types-setuptools": {
+ "hashes": [
+ "sha256:30a0d9903a81a424bd0f979534552a016a4543760aaffd499b9a2fe85bae0bfd",
+ "sha256:8a886a1fd06b668782dfbdaded4fd8a4e8c9f3d8d4c02acdd1240df098f50bf7"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==69.1.0.20240223"
+ },
+ "types-toml": {
+ "hashes": [
+ "sha256:58b0781c681e671ff0b5c0319309910689f4ab40e8a2431e205d70c94bb6efb1",
+ "sha256:61951da6ad410794c97bec035d59376ce1cbf4453dc9b6f90477e81e4442d631"
+ ],
+ "index": "pypi",
+ "version": "==0.10.8.7"
+ },
+ "types-urllib3": {
+ "hashes": [
+ "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f",
+ "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"
+ ],
+ "index": "pypi",
+ "version": "==1.26.25.14"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
+ "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.10.0"
+ },
+ "typing-inspect": {
+ "hashes": [
+ "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f",
+ "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"
+ ],
+ "version": "==0.9.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
+ "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.7"
+ },
+ "virtualenv": {
+ "hashes": [
+ "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a",
+ "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==20.25.1"
+ },
+ "wheel": {
+ "hashes": [
+ "sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d",
+ "sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==0.42.0"
+ },
+ "wrapt": {
+ "hashes": [
+ "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc",
+ "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81",
+ "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09",
+ "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e",
+ "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca",
+ "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0",
+ "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb",
+ "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487",
+ "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40",
+ "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c",
+ "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060",
+ "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202",
+ "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41",
+ "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9",
+ "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b",
+ "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664",
+ "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d",
+ "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362",
+ "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00",
+ "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc",
+ "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1",
+ "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267",
+ "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956",
+ "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966",
+ "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1",
+ "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228",
+ "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72",
+ "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d",
+ "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292",
+ "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0",
+ "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0",
+ "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36",
+ "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c",
+ "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5",
+ "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f",
+ "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73",
+ "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b",
+ "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2",
+ "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593",
+ "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39",
+ "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389",
+ "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf",
+ "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf",
+ "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89",
+ "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c",
+ "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c",
+ "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f",
+ "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440",
+ "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465",
+ "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136",
+ "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b",
+ "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8",
+ "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3",
+ "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8",
+ "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6",
+ "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e",
+ "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f",
+ "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c",
+ "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e",
+ "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8",
+ "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2",
+ "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020",
+ "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35",
+ "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d",
+ "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3",
+ "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537",
+ "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809",
+ "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d",
+ "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a",
+ "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==1.16.0"
+ }
+ }
+}
diff --git a/source/modules/acdp/__init__.py b/source/modules/acdp/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/backstage/.dockerignore b/source/modules/acdp/backstage/.dockerignore
similarity index 100%
rename from source/backstage/.dockerignore
rename to source/modules/acdp/backstage/.dockerignore
diff --git a/source/modules/acdp/backstage/.gitignore b/source/modules/acdp/backstage/.gitignore
new file mode 100644
index 00000000..11baec07
--- /dev/null
+++ b/source/modules/acdp/backstage/.gitignore
@@ -0,0 +1,48 @@
+# macOS
+.DS_Store
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Coverage directory generated when running tests with coverage
+coverage
+
+# Dependencies
+node_modules/
+
+# Yarn 3 files
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+
+# dotenv environment variables file
+.env
+.env.test
+
+# Build output
+dist
+dist-types
+
+# Temporary change files created by Vim
+*.swp
+
+# MkDocs build output
+site
+
+# # Local configuration files
+# *.local.yaml
+
+# Sensitive credentials
+*-credentials.yaml
+
+# vscode database functionality support files
+*.session.sql
diff --git a/source/backstage/.license-check.yaml b/source/modules/acdp/backstage/.license-check.yaml
similarity index 100%
rename from source/backstage/.license-check.yaml
rename to source/modules/acdp/backstage/.license-check.yaml
diff --git a/source/backstage/.prettierignore b/source/modules/acdp/backstage/.prettierignore
similarity index 100%
rename from source/backstage/.prettierignore
rename to source/modules/acdp/backstage/.prettierignore
diff --git a/source/backstage/LICENSE b/source/modules/acdp/backstage/LICENSE
similarity index 100%
rename from source/backstage/LICENSE
rename to source/modules/acdp/backstage/LICENSE
diff --git a/source/modules/acdp/backstage/README.md b/source/modules/acdp/backstage/README.md
new file mode 100644
index 00000000..80d8fb4b
--- /dev/null
+++ b/source/modules/acdp/backstage/README.md
@@ -0,0 +1,210 @@
+# Connected Mobility Solution on AWS - Backstage Module
+
+**[Connected Mobility Solution on AWS](https://aws.amazon.com/solutions/implementations/connected-mobility-solution-on-aws/)** | **[🚧 Feature request](https://github.com/aws-solutions/connected-mobility-solution-on-aws/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)** | **[🐛 Bug Report](https://github.com/aws-solutions/connected-mobility-solution-on-aws/issues/new?assignees=&labels=bug&template=bug_report.md&title=)** | **[❓ General Question](https://github.com/aws-solutions/connected-mobility-solution-on-aws/issues/new?assignees=&labels=question&template=general_question.md&title=)**
+
+**Note**: If you want to use the solution without building from source, navigate to the
+[AWS Solution Page](https://aws.amazon.com/solutions/implementations/connected-mobility-solution-on-aws/).
+
+## Table of Contents
+
+- [Connected Mobility Solution on AWS - Backstage Module](#connected-mobility-solution-on-aws---backstage-module)
+ - [Table of Contents](#table-of-contents)
+ - [Solution Overview](#solution-overview)
+ - [Architecture Diagram](#architecture-diagram)
+ - [Sequence Diagram](#sequence-diagram)
+ - [AWS CDK and Solutions Constructs](#aws-cdk-and-solutions-constructs)
+ - [Customizing the Module](#customizing-the-module)
+ - [Prerequisites](#prerequisites)
+ - [MacOS Installation Instructions](#macos-installation-instructions)
+ - [Clone the Repository](#clone-the-repository)
+ - [Install Required Dependencies](#install-required-dependencies)
+ - [Unit Test](#unit-test)
+ - [Build the Module](#build-the-module)
+ - [Upload Assets to S3](#upload-assets-to-s3)
+ - [Deploy on AWS](#deploy-on-aws)
+ - [Delete](#delete)
+ - [Local Development](#local-development)
+ - [Cost Scaling](#cost-scaling)
+ - [Collection of Operational Metrics](#collection-of-operational-metrics)
+ - [License](#license)
+
+## Solution Overview
+
+The ACDP Backstage Module is an opinionated deployment of [Backstage](https://backstage.io/). Backstage provides a convenient
+and functional interface to manage and deploy software. CMS modules are configured to be compatible with Backstage while
+enabling deeper features into the Backstage design.
+
+For more information and a detailed deployment guide, visit the
+[ACDP Backstage Module](https://docs.aws.amazon.com/solutions/latest/connected-mobility-solution-on-aws/backstage-module.html)
+Implementation Guide page.
+
+## Architecture Diagram
+
+![ACDP Backstage Architecture Diagram](./documentation/architecture/acdp-backstage-architecture-diagram.svg)
+
+## Sequence Diagram
+
+![CMS Module Deployment Sequence Diagram](./documentation/sequence/cms-module-deployment-sequence-diagram.svg)
+
+## AWS CDK and Solutions Constructs
+
+[AWS Cloud Development Kit (AWS CDK)](https://aws.amazon.com/cdk/) and
+[AWS Solutions Constructs](https://aws.amazon.com/solutions/constructs/) make it easier to consistently create
+well-architected infrastructure applications. All AWS Solutions Constructs are reviewed by AWS and use best
+practices established by the AWS Well-Architected Framework.
+
+In addition to the AWS Solutions Constructs, the solution uses AWS CDK directly to create infrastructure resources.
+
+## Customizing the Module
+
+## Prerequisites
+
+- [Python 3.8+](https://www.python.org/downloads/)
+- [NVM](https://github.com/nvm-sh/nvm)
+- [NPM 8+](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
+- [Node 18+](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
+- [Pipenv](https://pipenv.pypa.io/en/latest/installation.html)
+
+Required For Local Development Only:
+
+- [Docker](https://www.docker.com/products/docker-desktop/)
+- [Docker Compose v1](https://docs.docker.com/compose/install/) (v2 is included with Docker)
+
+### MacOS Installation Instructions
+
+Pyenv [Github Repository](https://github.com/pyenv/pyenv)
+
+```bash
+brew install pyenv
+pyenv install 3.10.9
+```
+
+Pipenv [Github Repository](https://github.com/pypa/pipenv)
+
+```bash
+pip install --user pipenv
+pipenv install --dev
+```
+
+NVM [Github Repository](https://github.com/nvm-sh/nvm)
+
+```bash
+curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
+```
+
+NPM/Node [Official Documentation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
+
+```bash
+nvm install 18
+nvm use 18
+```
+
+For local development:
+
+```bash
+brew cask install docker
+brew install docker-compose
+```
+
+### Clone the Repository
+
+```bash
+git clone https://github.com/aws-solutions/connected-mobility-solution-on-aws.git
+cd connected-mobility-solution-on-aws/source/modules/acdp/backstage/cdk/
+```
+
+### Install Required Dependencies
+
+```bash
+make install
+```
+
+### Unit Test
+
+After making changes, run unit tests to make sure added customization passes the tests:
+
+```bash
+make test
+```
+
+### Build the Module
+
+The build script manages dependencies, builds required assets (e.g. packaged lambdas), and creates the
+AWS Cloudformation templates.
+
+```bash
+make build
+```
+
+### Upload Assets to S3
+
+```bash
+make upload
+```
+
+### Deploy on AWS
+
+Deployment should be done via the ACDP module deployment. This deployment creates a CodePipeline instance that deploys Backstage.
+
+If manual deployment is desired, ensure ACDP is deployed and the proper environment variable configs are present and valid
+via the Backstage Makefile. Understand there is risk of unsuccessful config and deployment.
+
+```bash
+make deploy
+```
+
+### Delete
+
+```bash
+make destroy
+```
+
+### Local Development
+
+After installing dependencies, you can run backstage locally.
+Note: All commands assume the PWD is [git_root]/source/modules/acdp/backstage
+
+Start the postgres dev server
+
+```bash
+docker-compose up
+```
+
+Start the frontend and backend
+
+```bash
+yarn run dev
+```
+
+## Cost Scaling
+
+Cost will scale depending on the amount of templates and assets used, network traffic, and number of deployments.
+
+- [Amazon S3 Cost](https://aws.amazon.com/s3/pricing/)
+- [Amazon EC2 Cost](https://aws.amazon.com/ec2/pricing/)
+- [Amazon ELB Cost](https://aws.amazon.com/elasticloadbalancing/pricing/)
+- [Amazon CodeBuild Cost](https://aws.amazon.com/codebuild/pricing/)
+
+For more details, see the
+[implementation guide](https://docs.aws.amazon.com/solutions/latest/connected-mobility-solution-on-aws/cost.html).
+
+## Collection of Operational Metrics
+
+This solution collects anonymized operational metrics to help AWS improve
+the quality and features of the solution. For more information, including
+how to disable this capability, please see the
+[implementation guide](https://docs.aws.amazon.com/solutions/latest/connected-mobility-solution-on-aws/anonymized-data-collection.html).
+
+## License
+
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License").
+You may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/source/modules/acdp/backstage/__init__.py b/source/modules/acdp/backstage/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/app-config.local.yaml b/source/modules/acdp/backstage/app-config.local.yaml
new file mode 100644
index 00000000..3e3f7cbc
--- /dev/null
+++ b/source/modules/acdp/backstage/app-config.local.yaml
@@ -0,0 +1,200 @@
+app:
+ title: local
+ baseUrl: http://localhost:8081
+ auth:
+ providers: {}
+
+organization:
+ name: local
+
+acdp:
+ s3Catalog:
+ bucketName: ${REGIONAL_ASSET_BUCKET_NAME}
+ prefix: local/backstage/catalog
+ region: ${AWS_REGION}
+ buildConfig:
+ buildConfigStoreSsmPrefix: /local/backstage/acdp-build
+ deploymentDefaults:
+ codeBuildProjectArn: arn:aws:codebuild:${AWS_REGION}:${AWS_ACCOUNT_ID}:project/acdp-deployment-project
+ accountId: ${AWS_ACCOUNT_ID}
+ region: ${AWS_REGION}
+ metrics:
+ userAgentString: local-user-agent
+ allow-unsafe-local-dir-access: true
+
+backend:
+ # Used for enabling authentication, secret is shared by all backend plugins
+ # See https://backstage.io/docs/auth/service-to-service-auth for
+ # information on the format
+ auth:
+ keys:
+ - secret: test
+ providers: {}
+ baseUrl: http://localhost:8080
+ listen:
+ port: 8080
+ csp:
+ connect-src: ["'self'", 'http:', 'https:']
+ cors:
+ origin:
+ - http://localhost:8081
+ - http://localhost
+ methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
+ credentials: true
+ database:
+ client: pg
+ connection:
+ host: localhost
+ port: 5432
+ user: test
+ password: test
+ cache:
+ store: memory
+ # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
+
+# Reference documentation http://backstage.io/docs/features/techdocs/configuration
+# Note: After experimenting with basic setup, use CI/CD to generate docs
+# and an external cloud storage when deploying TechDocs for production use-case.
+# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach
+techdocs:
+ generator:
+ runIn: 'local'
+ builder: 'local'
+ publisher:
+ type: 'awsS3'
+ awsS3:
+ bucketName: ${REGIONAL_ASSET_BUCKET_NAME}
+ region: ${AWS_REGION}
+ bucketRootPath: local/backstage/techdocs
+auth:
+ environment: development
+ providers: {}
+ session:
+ secret: test
+ auth:
+ providers: {}
+ keys:
+ - secret: test
+
+scaffolder:
+ # see https://backstage.io/docs/features/software-templates/configuration for software template options
+ concurrentTasksLimit: 10
+
+catalog:
+ rules:
+ - allow: [Component, System, API, Group, User, Resource, Location, Template]
+ orphanStrategy: delete
+ processingInterval: { minutes: 1 }
+ locations:
+ - type: file
+ target: ../../../../../modules/cms_api/deployment/regional-s3-assets/backstage/templates/cms-api.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/cms_alerts/deployment/regional-s3-assets/backstage/templates/cms-alerts.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/cms_provisioning/deployment/regional-s3-assets/backstage/templates/cms-provisioning.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/cms_connect_store/deployment/regional-s3-assets/backstage/templates/cms-connect-store.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/cms_ev_battery_health/deployment/regional-s3-assets/backstage/templates/cms-ev-battery-health.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/cms_vehicle_simulator/deployment/regional-s3-assets/backstage/templates/cms-vehicle-simulator.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/cms_auth/deployment/regional-s3-assets/backstage/templates/cms-auth.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/cms_fleetwise_connector/deployment/regional-s3-assets/backstage/templates/cms-fleetwise-connector.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/vpc/deployment/regional-s3-assets/backstage/templates/vpc.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/auth_setup/deployment/regional-s3-assets/backstage/templates/auth-setup.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/cms_sample/deployment/regional-s3-assets/backstage/templates/cms-sample.template.yaml
+ rules:
+ - allow: [Template]
+ - type: file
+ target: ../../../../../modules/cms_config/deployment/regional-s3-assets/backstage/templates/cms-config.template.yaml
+ rules:
+ - allow: [Template]
+
+ # For local testing of AWS integration, uncomment this and fill in
+ # providers:
+ # rules:
+ # - allow: [Component, System, API, Group, User, Resource, Location, Template]
+ # awsS3:
+ # acdpTemplateResourceBucket:
+ # bucketName: ${REGIONAL_ASSET_BUCKET_NAME}
+ # prefix: ${BACKSTAGE_ASSETS_PREFIX}
+ # region: ${AWS_REGION}
+ # schedule:
+ # frequency:
+ # minutes: 1
+ # timeout: { minutes: 1 }
+ # acdpDocsResourceBucket:
+ # bucketName: ${REGIONAL_ASSET_BUCKET_NAME}
+ # prefix: ${BACKSTAGE_ASSETS_PREFIX}/docs
+ # region: ${AWS_REGION}
+ # schedule:
+ # frequency:
+ # minutes: 1
+ # timeout: { minutes: 3 }
+ # locations:
+ ## Uncomment these lines to add more example data
+ # - type: url
+ # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml
+
+ ## Uncomment these lines to add an example org
+ # - type: url
+ # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml
+ # rules:
+ # - allow: [User, Group]
+
+
+#Enable this to allow connections to externally hosted repositories. s3 integration works automatically via IAM and isn't needed here
+# integrations:
+# gitlab:
+# - host: gitlab.aws.dev
+# baseUrl: https://gitlab.aws.dev/
+# apiBaseUrl: https://gitlab.aws.dev/api/v4
+# token: ${GITLAB_TOKEN}
+# allowedKinds: [Component, System, API, Group, User, Resource, Location, Template]
+# github:
+# - host: github.com
+# # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information
+# # about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration
+# token: ${GITHUB_TOKEN}
+# allowedKinds: [Component, System, API, Group, User, Resource, Location, Template]
+### Example for how to add your GitHub Enterprise instance using the API:
+# - host: ghe.example.net
+# apiBaseUrl: https://ghe.example.net/api/v3
+# token: ${GHE_TOKEN}
+
+# proxy:
+# '/rss/reddit':
+# target: 'https://www.reddit.com/r/'
+# '/rss/hacker-news':
+# target: 'https://hnrss.org/'
+
+### Example for how to add a proxy endpoint for the frontend.
+### A typical reason to do this is to handle HTTPS and CORS for internal services.
+# '/test':
+# target: 'https://example.com'
+# changeOrigin: true
diff --git a/source/modules/acdp/backstage/app-config.production.yaml b/source/modules/acdp/backstage/app-config.production.yaml
new file mode 100644
index 00000000..8c0d41c9
--- /dev/null
+++ b/source/modules/acdp/backstage/app-config.production.yaml
@@ -0,0 +1,99 @@
+app:
+ title: ${BACKSTAGE_NAME}
+ baseUrl: https://${WEB_HOSTNAME}
+
+organization:
+ name: ${BACKSTAGE_ORG}
+
+backend:
+ auth:
+ keys:
+ - secret: ${BACKEND_SECRET}
+ baseUrl: https://${BACKEND_HOSTNAME}
+
+ listen:
+ port: 8080
+
+ csp:
+ connect-src: ["'self'", 'http:', 'https:']
+ cors:
+ origin:
+ - https://${WEB_HOSTNAME}:443
+ - https://${WEB_HOSTNAME}
+ methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
+ credentials: true
+ database:
+ client: pg
+ connection:
+ host: ${POSTGRES_HOST}
+ port: ${POSTGRES_PORT}
+ user: ${POSTGRES_USER}
+ password: ${POSTGRES_PASSWORD}
+ cache:
+ store: memory
+
+techdocs:
+ generator:
+ runIn: 'local'
+ builder: 'local'
+ publisher:
+ type: 'awsS3'
+ awsS3:
+ bucketName: ${LOCAL_ASSET_BUCKET_NAME}
+ region: ${LOCAL_ASSET_BUCKET_REGION}
+ bucketRootPath: ${LOCAL_ASSET_BUCKET_TECHDOCS_KEY_PREFIX}
+
+auth:
+ environment: production
+ session:
+ secret: ${BACKEND_SECRET}
+
+ auth:
+ keys:
+ - secret: ${BACKEND_SECRET}
+
+ providers:
+ cognito:
+ production:
+ userPoolId: ${COGNITO_USERPOOL_ID}
+ clientId: ${COGNITO_CLIENT_ID}
+
+scaffolder:
+ concurrentTasksLimit: 10
+
+acdp:
+ s3Catalog:
+ bucketName: ${LOCAL_ASSET_BUCKET_NAME}
+ prefix: ${LOCAL_ASSET_BUCKET_CATALOG_KEY_PREFIX}
+ region: ${LOCAL_ASSET_BUCKET_REGION}
+ buildConfig:
+ buildConfigStoreSsmPrefix: ${ACDP_BUILD_CONFIG_SSM_PREFIX}
+ deploymentDefaults:
+ codeBuildProjectArn: ${CODEBUILD_PROJECT_ARN}
+ accountId: ${TARGET_ACCOUNT_ID}
+ region: ${TARGET_REGION}
+ metrics:
+ userAgentString: ${USER_AGENT_STRING}
+
+catalog:
+ providers:
+ awsS3:
+ localAssetBucketUserProvidedTemplates:
+ bucketName: ${LOCAL_ASSET_BUCKET_NAME}
+ prefix: ${LOCAL_ASSET_BUCKET_BACKSTAGE_USER_PROVIDED_TEMPLATE_KEY_PREFIX}
+ region: ${LOCAL_ASSET_BUCKET_REGION}
+ schedule:
+ frequency:
+ minutes: ${LOCAL_ASSET_BUCKET_DISCOVERY_REFRESH_FREQ}
+ timeout: { minutes: 3 }
+ localAssetBucketDefaultTemplates:
+ bucketName: ${LOCAL_ASSET_BUCKET_NAME}
+ prefix: ${LOCAL_ASSET_BUCKET_BACKSTAGE_DEFAULT_TEMPLATE_KEY_PREFIX}
+ region: ${LOCAL_ASSET_BUCKET_REGION}
+ schedule:
+ frequency:
+ minutes: ${LOCAL_ASSET_BUCKET_DISCOVERY_REFRESH_FREQ}
+ timeout: { minutes: 3 }
+
+ rules:
+ - allow: [Component, System, API, Group, User, Resource, Location, Template]
diff --git a/source/modules/acdp/backstage/app-config.yaml b/source/modules/acdp/backstage/app-config.yaml
new file mode 100644
index 00000000..31f4e8e7
--- /dev/null
+++ b/source/modules/acdp/backstage/app-config.yaml
@@ -0,0 +1,11 @@
+app: {}
+
+organization: {}
+
+backend: {}
+
+techdocs: {}
+
+auth: {}
+
+catalog: {}
diff --git a/source/modules/acdp/backstage/backstage.json b/source/modules/acdp/backstage/backstage.json
new file mode 100644
index 00000000..dd154985
--- /dev/null
+++ b/source/modules/acdp/backstage/backstage.json
@@ -0,0 +1,3 @@
+{
+ "version": "1.23.4"
+}
diff --git a/source/backstage/cdk/.license-check.yaml b/source/modules/acdp/backstage/cdk/.license-check.yaml
similarity index 100%
rename from source/backstage/cdk/.license-check.yaml
rename to source/modules/acdp/backstage/cdk/.license-check.yaml
diff --git a/source/modules/acdp/backstage/cdk/.nvmrc b/source/modules/acdp/backstage/cdk/.nvmrc
new file mode 100644
index 00000000..aacb5181
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/.nvmrc
@@ -0,0 +1 @@
+18.17
diff --git a/source/modules/acdp/backstage/cdk/.python-version b/source/modules/acdp/backstage/cdk/.python-version
new file mode 100644
index 00000000..c8cfe395
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/.python-version
@@ -0,0 +1 @@
+3.10
diff --git a/source/modules/acdp/backstage/cdk/Makefile b/source/modules/acdp/backstage/cdk/Makefile
new file mode 100644
index 00000000..9f0ce86f
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/Makefile
@@ -0,0 +1,232 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+.DEFAULT_GOAL := help
+
+# ========================================================
+# AWS CONFIGURATION
+# ========================================================
+DEFAULTS.AWS_ACCOUNT_ID := $(shell aws sts get-caller-identity --query "Account" --output text)
+DEFAULTS.AWS_REGION := $(shell aws configure get region --output text)
+
+export AWS_ACCOUNT_ID ?= ${DEFAULTS.AWS_ACCOUNT_ID}
+export AWS_REGION ?= ${DEFAULTS.AWS_REGION}
+
+# ========================================================
+# SOLUTION METADATA
+# ========================================================
+export SOLUTION_NAME ?= connected-mobility-solution-on-aws
+export SOLUTION_DESCRIPTION ?= Accelerate development and deployment of connected vehicle assets with purpose-built, deployment-ready accelerators, and an Automotive Cloud Developer Portal
+export SOLUTION_VERSION ?= v1.1.0
+export SOLUTION_AUTHOR = AWS Industrial Solutions Team
+export SOLUTION_ID = SO0241
+export APPLICATION_TYPE = AWS-Solutions
+
+# ========================================================
+# ENVIRONMENT CONFIGURATION
+# ========================================================
+DEFAULTS.NODE_VERSION := $(shell cat .nvmrc 2> /dev/null)
+DEFAULTS.PYTHON_VERSION := $(shell cat .python-version)
+
+export NODE_VERSION ?= ${DEFAULTS.NODE_VERSION}
+export PYTHON_VERSION ?= ${DEFAULTS.PYTHON_VERSION}
+
+export PYTHON_MINIMUM_VERSION_SUPPORTED = 3.10
+export PIPENV_IGNORE_VIRTUALENVS = 1
+export PIPENV_VENV_IN_PROJECT = 1
+export LANG = en_US.UTF-8
+
+# ========================================================
+# VARIABLES
+# ========================================================
+export REGIONAL_ASSET_BUCKET_BASE_NAME ?= acdp-assets-${AWS_ACCOUNT_ID}
+export REGIONAL_ASSET_BUCKET_NAME ?= ${REGIONAL_ASSET_BUCKET_BASE_NAME}-${AWS_REGION}
+export GLOBAL_ASSET_BUCKET_NAME ?= ${REGIONAL_ASSET_BUCKET_NAME}
+
+# Using a ?= here fails to update the variable when this file is imported from each module makefile during a makefile chain
+export S3_ASSET_KEY_PREFIX = ${SOLUTION_NAME}/${SOLUTION_VERSION}/${MODULE_NAME}
+
+# Used by CDK apps
+export S3_ASSET_BUCKET_BASE_NAME ?= ${REGIONAL_ASSET_BUCKET_BASE_NAME}
+
+# ==================================================================================
+# PRINT COLORS
+# To use, simply add ${} to get the colored text.
+# To disable color, add ${NC} at the point you'd like it to stop.
+# printf is recommended over echo if wanting color because of more multi-platform support.
+# https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+# ==================================================================================
+export RED = \033[0;31m
+export GREEN = \033[0;32m
+export YELLOW = \033[0;33m
+export BLUE = \033[0;34m
+export MAGENTA = \033[0;35m
+export CYAN = \033[0;36m
+export NC = \033[00m
+
+
+# ========================================================
+# SOLUTION METADATA
+# ========================================================
+export MODULE_NAME ?= acdp-backstage
+export MODULE_SHORT_NAME ?= ${MODULE_NAME}
+export MODULE_VERSION ?= ${SOLUTION_VERSION}
+export MODULE_DESCRIPTION ?= A CDK Python app to provision Spotify Backstage
+export MODULE_AUTHOR ?= AWS Industrial Solutions Team
+MODULE_PATH := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
+
+export GLOBAL_ASSET_BUCKET_REGION = $(shell BUCKET=${GLOBAL_ASSET_BUCKET_NAME} ${MODULE_PATH}/deployment/determine-bucket-region.sh)
+export REGIONAL_ASSET_BUCKET_REGION = $(shell BUCKET=${REGIONAL_ASSET_BUCKET_NAME} ${MODULE_PATH}/deployment/determine-bucket-region.sh)
+
+# ========================================================
+# ENVIRONMENT CONFIGURATION
+# ========================================================
+export NODE_VERSION := $(shell cat .nvmrc)
+export PIPENV_IGNORE_VIRTUALENVS = 1
+export PIPENV_VENV_IN_PROJECT = 1
+export PYTHON_VERSION := $(shell cat .python-version)
+export PYTHON_MINIMUM_VERSION_SUPPORTED = 3.10
+
+# ========================================================
+# VARIABLES
+# ========================================================
+
+export ACDP_UNIQUE_ID ?= acdp
+
+export STACK_NAME ?= ${ACDP_UNIQUE_ID}--${MODULE_NAME}
+export STACK_TEMPLATE_NAME = ${MODULE_NAME}.template
+export STACK_TEMPLATE_PATH ?= deployment/global-s3-assets/${MODULE_NAME}/${STACK_TEMPLATE_NAME}
+export S3_ASSET_KEY_PREFIX ?= ${SOLUTION_NAME}/${SOLUTION_VERSION}/${MODULE_NAME}
+
+export CAPABILITY_ID ?= CMS.6
+
+export BACKSTAGE_IMAGE_TAG ?= latest
+
+export ROUTE53_HOSTED_ZONE_NAME ?= $(shell aws ssm get-parameter --name /solution/${ACDP_UNIQUE_ID}/config/route53/zone-name --with-decryption --query "Parameter.Value" --output text 2> /dev/null)
+export ROUTE53_BASE_DOMAIN ?= $(shell aws ssm get-parameter --name /solution/${ACDP_UNIQUE_ID}/config/route53/base-domain --with-decryption --query "Parameter.Value" --output text 2> /dev/null)
+
+# Backstage is built directly via codepipeline,
+# so use local asset bucket instead of public ones when running in this way.
+export LOCAL_ASSET_BUCKET_NAME ?= ${REGIONAL_ASSET_BUCKET_BASE_NAME}-${AWS_REGION}
+export GLOBAL_ASSET_BUCKET_NAME = ${LOCAL_ASSET_BUCKET_NAME}
+export REGIONAL_ASSET_BUCKET_NAME = ${LOCAL_ASSET_BUCKET_NAME}
+
+
+.PHONY: install
+install: pipenv-install ## Installs the resources and dependencies required to build the solution.
+ @printf "%bInstall finished.%b\n" "${GREEN}" "${NC}"
+
+.PHONY: deploy
+deploy: ## Deploy the stack for the module.
+ @printf "%bDeploy the module.%b\n" "${MAGENTA}" "${NC}"
+ aws cloudformation deploy \
+ --stack-name ${STACK_NAME} \
+ --template-file ${STACK_TEMPLATE_PATH} \
+ --s3-bucket ${GLOBAL_ASSET_BUCKET_NAME} \
+ --s3-prefix ${SOLUTION_NAME}/local/${MODULE_NAME} \
+ --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \
+ --parameter-overrides \
+ "AcdpUniqueId"="${ACDP_UNIQUE_ID}" \
+ "VpcName"="${VPC_NAME}" \
+ ${shell [ -n "${CLOUDFORMATION_ROLE_ARN}" ] && echo "--role-arn ${CLOUDFORMATION_ROLE_ARN}"}
+
+.PHONY: destroy
+destroy: destroy-stack ## Teardown deployed stack
+ @printf "%bDestroy finished.%b\n" "${GREEN}" "${NC}"
+
+## ========================================================
+## COMMON TARGETS
+## ========================================================
+.PHONY: pipenv-install
+pipenv-install: ## Using pipenv, installs pip dependencies.
+ @printf "%bInstalling pip dependencies: %s%b\n" "${MAGENTA}" "${MODULE_NAME}" "${NC}"
+ @pipenv install --dev --python ${PYTHON_VERSION}
+ @pipenv clean --python ${PYTHON_VERSION}
+
+.PHONY: build
+build: verify-required-tools ## Build templates and assets for the module.
+ @printf "%bBuilding the module.%b\n" "${MAGENTA}" "${NC}"
+ ${MODULE_PATH}/deployment/build-s3-dist.sh
+
+.PHONY: upload
+upload: ## Upload templates and build assets for the module to S3 buckets.
+ @printf "%bUploading S3 assets for the module.%b\n" "${MAGENTA}" "${NC}"
+ ${MODULE_PATH}/deployment/upload-s3-dist.sh
+
+.PHONY: destroy-stack
+destroy-stack: ## Delete the stack for the module.
+ @printf "%bDelete the module deployment.%b\n" "${MAGENTA}" "${NC}"
+ @aws cloudformation delete-stack --stack-name "${STACK_NAME}"
+ @aws cloudformation wait stack-delete-complete --stack-name "${STACK_NAME}"
+
+.PHONY: all
+all: build upload deploy ## Rebuild modules, upload assets to s3, and deploy
+
+## ========================================================
+## TESTING
+## ========================================================
+.PHONY: verify-module
+verify-module: cfn-nag unit-tests ## Run all testing for the module.
+ @printf "%bFinished testing.%b\n" "${MAGENTA}" "${NC}"
+
+.PHONY: test
+test: cfn-nag unit-tests ## Run all testing for the module.
+ @printf "%bFinished testing.%b\n" "${MAGENTA}" "${NC}"
+
+.PHONY: cfn-nag
+cfn-nag: ## Run cfn-nag for the module.
+ @printf "%bRunning cfn-nag checks.%b\n" "${MAGENTA}" "${NC}"
+ -${MODULE_PATH}/deployment/run-cfn-nag.sh
+
+.PHONY: unit-tests
+unit-tests: ## Run unit-tests for the module.
+ @printf "%bRunning unit tests.%b\n" "${MAGENTA}" "${NC}"
+ -${MODULE_PATH}/deployment/run-unit-tests.sh
+
+.PHONY: update-snapshots
+update-snapshots: ## Update snapshot files for the module.
+ @printf "%bUpdating unit test snapshots.%b\n" "${MAGENTA}" "${NC}"
+ -${MODULE_PATH}/deployment/run-unit-tests.sh -r -s
+
+## ========================================================
+## HELP COMMANDS
+## ========================================================
+.PHONY: help
+help: ## Displays this help message.
+ @grep -E '^[a-zA-Z0-9 -]+:.*##|^##.*' ${MODULE_PATH}/Makefile | while read -r l; \
+ do ( [[ "$$l" =~ ^"##" ]] && printf "%b%s%b\n" "${MAGENTA}" "$$(echo $$l | cut -f 2- -d' ')" "${NC}") \
+ || ( printf "%b%-35s%s%b\n" "${GREEN}" "$$(echo $$l | cut -f 1 -d':')" "$$(echo $$l | cut -f 3- -d'#')" "${NC}"); \
+ done;
+
+.PHONY: print-module-name
+print-module-name: ## Used to get module name safely from any directory if used with make -C
+ @printf "${MODULE_NAME}"
+
+.PHONY: version
+version: ## Display module name and current version
+ @printf "%b%35.35s%b version:%b%s%b\n" $$( [[ "${MODULE_PATH}" = *"lib"* ]] && echo "${YELLOW}" || echo "${CYAN}" ) "${MODULE_NAME}" "${NC}" "${GREEN}" "${MODULE_VERSION}" "${NC}"
+
+.PHONY: verify-required-tools
+verify-required-tools: ## Checks the environment for the required dependencies.
+ifneq (v${NODE_VERSION}, $(shell node --version | cut -d "." -f 1-2))
+ $(error Node version "v${NODE_VERSION}" is required, as specified in .nvmrc. "$(shell node --version | cut -d "." -f 1-2)" was found instead. Please install the correct version by running `nvm install`.)
+endif
+ifeq (, $(shell which npm))
+ $(error Npm is required and should be automatically installed with node. Please check your node installation.`)
+endif
+ifeq (, $(shell which yarn))
+ $(error Yarn is required, as specified in the README. Please see the following link for installation (OS specific): https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable)
+endif
+ifneq (Python ${PYTHON_VERSION}, $(shell python --version | cut -d "." -f 1-2))
+ $(error Python version "Python ${PYTHON_VERSION}" is required, as specified in .python-version. "$(shell python --version | cut -d "." -f 1-2)" was found instead. Please install the correct version by running `pyenv install -s`)
+endif
+ifeq (, $(shell which pipenv))
+ $(error pipenv is required, as specified in the README. Please see the following link for installation: https://pipenv.pypa.io/en/latest/installation.html)
+endif
+ifeq (, $(shell which aws))
+ $(error The aws CLI is required, as specified in the README. Please see the following link for installation: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html)
+endif
+ifeq (, $(shell which cdk))
+ $(error The aws-cdk CLI is required, as specified in the README. Please see the following link for installation: https://docs.aws.amazon.com/cdk/v2/guide/cli.html)
+endif
+ @printf "%bDependencies verified.%b\n" "${GREEN}" "${NC}"
diff --git a/source/modules/acdp/backstage/cdk/Pipfile b/source/modules/acdp/backstage/cdk/Pipfile
new file mode 100644
index 00000000..ee06383b
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/Pipfile
@@ -0,0 +1,28 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+
+[dev-packages]
+aws-cdk-lib = ">=2.63.2"
+types-boto3 = ">=1.0.2"
+types-pyyaml = "*"
+types-setuptools = ">=65.6.0.1"
+pytest = "*"
+pytest-mock = "*"
+mypy = "*"
+pycln = "*"
+moto = "*"
+pytest-cov = "*"
+pre-commit = "*"
+pyjsparser = "*"
+pylint = "*"
+cdk-nag = "*"
+zipp = "*"
+syrupy = "*"
+wheel = "*"
+
+[requires]
+python_version = "3.10"
diff --git a/source/modules/acdp/backstage/cdk/Pipfile.lock b/source/modules/acdp/backstage/cdk/Pipfile.lock
new file mode 100644
index 00000000..15c0d98d
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/Pipfile.lock
@@ -0,0 +1,1023 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "14bcac98a0be7abe0da974ee8939487dc54be8a59785c5c4f0752b577a1ae266"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.10"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {},
+ "develop": {
+ "astroid": {
+ "hashes": [
+ "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819",
+ "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"
+ ],
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==3.1.0"
+ },
+ "attrs": {
+ "hashes": [
+ "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30",
+ "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==23.2.0"
+ },
+ "aws-cdk-lib": {
+ "hashes": [
+ "sha256:03a98770dd58caa002ded8d2dcdd3f6f7451a95f86c8dba3b5f2b70e659429b3",
+ "sha256:b9ed68a5fd7f5b9056da58bd122c9c3faa6af1e92f4b6aff181a2ee57625aad1"
+ ],
+ "index": "pypi",
+ "markers": "python_version ~= '3.8'",
+ "version": "==2.130.0"
+ },
+ "aws-cdk.asset-awscli-v1": {
+ "hashes": [
+ "sha256:3ef87d6530736b3a7b0f777fe3b4297994dd40c3ce9306d95f80f48fb18036e8",
+ "sha256:96205ea2e5e132ec52fabfff37ea25b9b859498f167d05b32564c949822cd331"
+ ],
+ "markers": "python_version ~= '3.8'",
+ "version": "==2.2.202"
+ },
+ "aws-cdk.asset-kubectl-v20": {
+ "hashes": [
+ "sha256:346283e43018a43e3b3ca571de3f44e85d49c038dc20851894cb8f9b2052b164",
+ "sha256:7f0617ab6cb942b066bd7174bf3e1f377e57878c3e1cddc21d6b2d13c92d0cc1"
+ ],
+ "markers": "python_version ~= '3.7'",
+ "version": "==2.1.2"
+ },
+ "aws-cdk.asset-node-proxy-agent-v6": {
+ "hashes": [
+ "sha256:42cdbc1de2ed3f845e3eb883a72f58fc7e5554c2e0b6fcdb366c159778dce74d",
+ "sha256:e442673d4f93137ab165b75386761b1d46eea25fc5015e5145ae3afa9da06b6e"
+ ],
+ "markers": "python_version ~= '3.7'",
+ "version": "==2.0.1"
+ },
+ "boto3": {
+ "hashes": [
+ "sha256:2cd9463e738a184cbce8a6824027c22163c5f73e277a35ff5aa0fb0e845b4301",
+ "sha256:67732634dc7d0afda879bd9a5e2d0818a2c14a98bef766b95a3e253ea5104cb9"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "boto3-stubs": {
+ "hashes": [
+ "sha256:3c3283d3982099cfbe6fee474f8eae42217b7cdfd98d5dd857ea952e29bdabf1",
+ "sha256:c04ece156a376745af34aefe7283e93f7066d8f2be2500297b129e3d46e0ac26"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "botocore": {
+ "hashes": [
+ "sha256:01d5156247f991b3466a8404e3d7460a9ecbd9b214f9992d6ba797d9ddc6f120",
+ "sha256:5086217442e67dd9de36ec7e87a0c663f76b7790d5fb6a12de565af95e87e319"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.34.51"
+ },
+ "botocore-stubs": {
+ "hashes": [
+ "sha256:8748b9fe01f66bb1e7b13f45e3336e2e2c5460d232816d45941573425459c66e",
+ "sha256:d0f4d9859d9f6affbe4b0b46e37fe729860eaab55ebefe7e09cf567396b2feda"
+ ],
+ "markers": "python_version >= '3.8' and python_version < '4.0'",
+ "version": "==1.34.51"
+ },
+ "cattrs": {
+ "hashes": [
+ "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108",
+ "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==23.2.3"
+ },
+ "cdk-nag": {
+ "hashes": [
+ "sha256:602d8a91252424f557f2dc991dca413dbdd7ae656303d961a849634a4181532a",
+ "sha256:8f62603886eac9072aa77fc79700efdc6d1ac44a7b8537516f8adf849d59dae9"
+ ],
+ "index": "pypi",
+ "markers": "python_version ~= '3.8'",
+ "version": "==2.28.48"
+ },
+ "certifi": {
+ "hashes": [
+ "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
+ "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==2024.2.2"
+ },
+ "cffi": {
+ "hashes": [
+ "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc",
+ "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a",
+ "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417",
+ "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab",
+ "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520",
+ "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36",
+ "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743",
+ "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8",
+ "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed",
+ "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684",
+ "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56",
+ "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324",
+ "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d",
+ "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235",
+ "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e",
+ "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088",
+ "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000",
+ "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7",
+ "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e",
+ "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673",
+ "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c",
+ "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe",
+ "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2",
+ "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098",
+ "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8",
+ "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a",
+ "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0",
+ "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b",
+ "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896",
+ "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e",
+ "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9",
+ "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2",
+ "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b",
+ "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6",
+ "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404",
+ "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f",
+ "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0",
+ "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4",
+ "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc",
+ "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936",
+ "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba",
+ "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872",
+ "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb",
+ "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614",
+ "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1",
+ "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d",
+ "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969",
+ "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b",
+ "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4",
+ "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627",
+ "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956",
+ "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"
+ ],
+ "markers": "platform_python_implementation != 'PyPy'",
+ "version": "==1.16.0"
+ },
+ "cfgv": {
+ "hashes": [
+ "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9",
+ "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.4.0"
+ },
+ "charset-normalizer": {
+ "hashes": [
+ "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027",
+ "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087",
+ "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786",
+ "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8",
+ "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09",
+ "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185",
+ "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574",
+ "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e",
+ "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519",
+ "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898",
+ "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269",
+ "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3",
+ "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f",
+ "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6",
+ "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8",
+ "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a",
+ "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73",
+ "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc",
+ "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714",
+ "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2",
+ "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc",
+ "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce",
+ "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d",
+ "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e",
+ "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6",
+ "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269",
+ "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96",
+ "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d",
+ "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a",
+ "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4",
+ "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77",
+ "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d",
+ "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0",
+ "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed",
+ "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068",
+ "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac",
+ "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25",
+ "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8",
+ "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab",
+ "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26",
+ "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2",
+ "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db",
+ "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f",
+ "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5",
+ "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99",
+ "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c",
+ "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d",
+ "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811",
+ "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa",
+ "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a",
+ "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03",
+ "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b",
+ "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04",
+ "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c",
+ "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001",
+ "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458",
+ "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389",
+ "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99",
+ "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985",
+ "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537",
+ "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238",
+ "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f",
+ "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d",
+ "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796",
+ "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a",
+ "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143",
+ "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8",
+ "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c",
+ "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5",
+ "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5",
+ "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711",
+ "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4",
+ "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6",
+ "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c",
+ "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7",
+ "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4",
+ "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b",
+ "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae",
+ "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12",
+ "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c",
+ "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae",
+ "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8",
+ "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887",
+ "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b",
+ "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4",
+ "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f",
+ "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5",
+ "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33",
+ "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519",
+ "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==3.3.2"
+ },
+ "click": {
+ "hashes": [
+ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+ "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==8.1.7"
+ },
+ "constructs": {
+ "hashes": [
+ "sha256:2972f514837565ff5b09171cfba50c0159dfa75ee86a42921ea8c86f2941b3d2",
+ "sha256:518551135ec236f9cc6b86500f4fbbe83b803ccdc6c2cb7684e0b7c4d234e7b1"
+ ],
+ "markers": "python_version ~= '3.7'",
+ "version": "==10.3.0"
+ },
+ "coverage": {
+ "extras": [
+ "toml"
+ ],
+ "hashes": [
+ "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa",
+ "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003",
+ "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f",
+ "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c",
+ "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e",
+ "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0",
+ "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9",
+ "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52",
+ "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e",
+ "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454",
+ "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0",
+ "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079",
+ "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352",
+ "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f",
+ "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30",
+ "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe",
+ "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113",
+ "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765",
+ "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc",
+ "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e",
+ "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501",
+ "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7",
+ "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2",
+ "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f",
+ "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4",
+ "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524",
+ "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c",
+ "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51",
+ "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840",
+ "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6",
+ "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee",
+ "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e",
+ "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45",
+ "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba",
+ "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d",
+ "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3",
+ "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10",
+ "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e",
+ "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb",
+ "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9",
+ "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a",
+ "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47",
+ "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1",
+ "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3",
+ "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914",
+ "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328",
+ "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6",
+ "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d",
+ "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0",
+ "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94",
+ "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc",
+ "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==7.4.3"
+ },
+ "cryptography": {
+ "hashes": [
+ "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee",
+ "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576",
+ "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d",
+ "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30",
+ "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413",
+ "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb",
+ "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da",
+ "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4",
+ "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd",
+ "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc",
+ "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8",
+ "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1",
+ "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc",
+ "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e",
+ "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8",
+ "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940",
+ "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400",
+ "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7",
+ "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16",
+ "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278",
+ "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74",
+ "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec",
+ "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1",
+ "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2",
+ "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c",
+ "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922",
+ "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a",
+ "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6",
+ "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1",
+ "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e",
+ "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac",
+ "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==42.0.5"
+ },
+ "dill": {
+ "hashes": [
+ "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca",
+ "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==0.3.8"
+ },
+ "distlib": {
+ "hashes": [
+ "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784",
+ "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"
+ ],
+ "version": "==0.3.8"
+ },
+ "exceptiongroup": {
+ "hashes": [
+ "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14",
+ "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==1.2.0"
+ },
+ "filelock": {
+ "hashes": [
+ "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e",
+ "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.13.1"
+ },
+ "identify": {
+ "hashes": [
+ "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791",
+ "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==2.5.35"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
+ "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==3.6"
+ },
+ "importlib-resources": {
+ "hashes": [
+ "sha256:308abf8474e2dba5f867d279237cd4076482c3de7104a40b41426370e891549b",
+ "sha256:9a0a862501dc38b68adebc82970140c9e4209fc99601782925178f8386339938"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==6.1.2"
+ },
+ "iniconfig": {
+ "hashes": [
+ "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
+ "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.0"
+ },
+ "isort": {
+ "hashes": [
+ "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109",
+ "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"
+ ],
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==5.13.2"
+ },
+ "jinja2": {
+ "hashes": [
+ "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
+ "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==3.1.3"
+ },
+ "jmespath": {
+ "hashes": [
+ "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
+ "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.0.1"
+ },
+ "jsii": {
+ "hashes": [
+ "sha256:1105bae271ae47c27cf31c1565c5157306efed5ad9323c9a27336f962f465716",
+ "sha256:175abc356603d98f18ab6f6aa74bfeae253e4e56340aef9dc40bbb1a6a59868b"
+ ],
+ "markers": "python_version ~= '3.8'",
+ "version": "==1.94.0"
+ },
+ "libcst": {
+ "hashes": [
+ "sha256:0cb92398236566f0b73a0c73f8a41a9c4906c793e8f7c2745f30e3fb141a34b5",
+ "sha256:13ca9fe82326d82feb2c7b0f5a320ce7ed0d707c32919dd36e1f40792459bf6f",
+ "sha256:1b5fecb2b26fa3c1efe6e05ef1420522bd31bb4dae239e4c41fdf3ddbd853aeb",
+ "sha256:1d45718f7e7a1405a16fd8e7fc75c365120001b6928bfa3c4112f7e533990b9a",
+ "sha256:2bbb4e442224da46b59a248d7d632ed335eae023a921dea1f5c72d2a059f6be9",
+ "sha256:38fbd56f885e1f77383a6d1d798a917ffbc6d28dc6b1271eddbf8511c194213e",
+ "sha256:3c7c0edfe3b878d64877671261c7b3ffe9d23181774bfad5d8fcbdbbbde9f064",
+ "sha256:4973a9d509cf1a59e07fac55a98f70bc4fd35e09781dffb3ec93ee32fc0de7af",
+ "sha256:5c0d548d92c6704bb07ce35d78c0e054cdff365def0645c1b57c856c8e112bb4",
+ "sha256:5e54389abdea995b39ee96ad736ed1b0b8402ed30a7956b7a279c10baf0c0294",
+ "sha256:6dd388c74c04434b41e3b25fc4a0fafa3e6abf91f97181df55e8f8327fd903cc",
+ "sha256:71dd69fff76e7edaf8fae0f63ffcdbf5016e8cd83165b1d0688d6856aa48186a",
+ "sha256:7f4919978c2b395079b64d8a654357854767adbabab13998b39c1f0bc67da8a7",
+ "sha256:82373a35711a8bb2a664dba2b7aeb20bbcce92a4db40af964e9cb2b976f989e7",
+ "sha256:8b56130f18aca9a98b3bcaf5962b2b26c2dcdd6d5132decf3f0b0b635f4403ba",
+ "sha256:968b93400e66e6711a29793291365e312d206dbafd3fc80219cfa717f0f01ad5",
+ "sha256:b4066dcadf92b183706f81ae0b4342e7624fc1d9c5ca2bf2b44066cb74bf863f",
+ "sha256:ba24b8cf789db6b87c6e23a6c6365f5f73cb7306d929397581d5680149e9990c",
+ "sha256:c0149d24a455536ff2e41b3a48b16d3ebb245e28035013c91bd868def16592a0",
+ "sha256:c80f36f4a02d530e28eac7073aabdea7c6795fc820773a02224021d79d164e8b",
+ "sha256:dded0e4f2e18150c4b07fedd7ef84a9abc7f9bd2d47cc1c485248ee1ec58e5cc",
+ "sha256:dece0362540abfc39cd2cf5c98cde238b35fd74a1b0167e2563e4b8bb5f47489",
+ "sha256:e01879aa8cd478bb8b1e4285cfd0607e64047116f7ab52bc2a787cde584cd686",
+ "sha256:f080e9af843ff609f8f35fc7275c8bf08b02c31115e7cd5b77ca3b6a56c75096",
+ "sha256:f2342634f6c61fc9076dc0baf21e9cf5ef0195a06e1e95c0c9dc583ba3a30d00"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==1.2.0"
+ },
+ "markupsafe": {
+ "hashes": [
+ "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
+ "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
+ "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
+ "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
+ "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
+ "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
+ "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
+ "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
+ "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
+ "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
+ "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
+ "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
+ "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
+ "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
+ "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
+ "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
+ "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
+ "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
+ "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
+ "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
+ "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
+ "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
+ "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
+ "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
+ "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
+ "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
+ "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
+ "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
+ "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
+ "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
+ "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
+ "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
+ "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
+ "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
+ "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
+ "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
+ "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
+ "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
+ "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
+ "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
+ "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
+ "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
+ "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
+ "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
+ "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
+ "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
+ "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
+ "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
+ "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
+ "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
+ "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
+ "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
+ "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
+ "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
+ "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
+ "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
+ "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
+ "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
+ "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
+ "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.1.5"
+ },
+ "mccabe": {
+ "hashes": [
+ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
+ "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.7.0"
+ },
+ "moto": {
+ "hashes": [
+ "sha256:71bb832a18b64f10fc4cec117b9b0e2305e5831d9a17eb74f6b9819ed7613843",
+ "sha256:7e27395e5c63ff9554ae14b5baa41bfe6d6b1be0e59eb02977c6ce28411246de"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==5.0.2"
+ },
+ "mypy": {
+ "hashes": [
+ "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6",
+ "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d",
+ "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02",
+ "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d",
+ "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3",
+ "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3",
+ "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3",
+ "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66",
+ "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259",
+ "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835",
+ "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd",
+ "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d",
+ "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8",
+ "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07",
+ "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b",
+ "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e",
+ "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6",
+ "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae",
+ "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9",
+ "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d",
+ "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a",
+ "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592",
+ "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218",
+ "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817",
+ "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4",
+ "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410",
+ "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==1.8.0"
+ },
+ "mypy-extensions": {
+ "hashes": [
+ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
+ "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==1.0.0"
+ },
+ "nodeenv": {
+ "hashes": [
+ "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2",
+ "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
+ "version": "==1.8.0"
+ },
+ "packaging": {
+ "hashes": [
+ "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
+ "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==23.2"
+ },
+ "pathspec": {
+ "hashes": [
+ "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
+ "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.12.1"
+ },
+ "platformdirs": {
+ "hashes": [
+ "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068",
+ "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.2.0"
+ },
+ "pluggy": {
+ "hashes": [
+ "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981",
+ "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.4.0"
+ },
+ "pre-commit": {
+ "hashes": [
+ "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c",
+ "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==3.6.2"
+ },
+ "publication": {
+ "hashes": [
+ "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6",
+ "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"
+ ],
+ "version": "==0.0.3"
+ },
+ "pycln": {
+ "hashes": [
+ "sha256:1f3eefb7be18a9ee06c3bdd0ba2e91218cd39317e20130325f107e96eb84b9f6",
+ "sha256:d1bf648df17077306100815d255d45430035b36f66bac635df04a323c61ba126"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.7.0' and python_version < '4'",
+ "version": "==2.4.0"
+ },
+ "pycparser": {
+ "hashes": [
+ "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
+ "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
+ ],
+ "version": "==2.21"
+ },
+ "pyjsparser": {
+ "hashes": [
+ "sha256:2b12842df98d83f65934e0772fa4a5d8b123b3bc79f1af1789172ac70265dd21",
+ "sha256:be60da6b778cc5a5296a69d8e7d614f1f870faf94e1b1b6ac591f2ad5d729579"
+ ],
+ "index": "pypi",
+ "version": "==2.7.1"
+ },
+ "pylint": {
+ "hashes": [
+ "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74",
+ "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==3.1.0"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd",
+ "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==8.0.2"
+ },
+ "pytest-cov": {
+ "hashes": [
+ "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6",
+ "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==4.1.0"
+ },
+ "pytest-mock": {
+ "hashes": [
+ "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f",
+ "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==3.12.0"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==2.8.2"
+ },
+ "pyyaml": {
+ "hashes": [
+ "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
+ "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+ "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
+ "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+ "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+ "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+ "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+ "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+ "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+ "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+ "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
+ "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
+ "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+ "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
+ "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+ "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+ "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+ "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+ "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+ "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+ "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+ "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
+ "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+ "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+ "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+ "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
+ "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
+ "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+ "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+ "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
+ "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+ "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+ "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+ "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+ "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+ "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+ "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+ "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+ "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+ "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+ "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+ "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+ "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
+ "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+ "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
+ "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+ "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+ "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+ "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+ "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+ "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==6.0.1"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
+ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.31.0"
+ },
+ "responses": {
+ "hashes": [
+ "sha256:01ae6a02b4f34e39bffceb0fc6786b67a25eae919c6368d05eabc8d9576c2a66",
+ "sha256:2f0b9c2b6437db4b528619a77e5d565e4ec2a9532162ac1a131a83529db7be1a"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.25.0"
+ },
+ "s3transfer": {
+ "hashes": [
+ "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e",
+ "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.10.0"
+ },
+ "setuptools": {
+ "hashes": [
+ "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56",
+ "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==69.1.1"
+ },
+ "six": {
+ "hashes": [
+ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+ "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==1.16.0"
+ },
+ "syrupy": {
+ "hashes": [
+ "sha256:203e52f9cb9fa749cf683f29bd68f02c16c3bc7e7e5fe8f2fc59bdfe488ce133",
+ "sha256:37a835c9ce7857eeef86d62145885e10b3cb9615bc6abeb4ce404b3f18e1bb36"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.8.1' and python_version < '4'",
+ "version": "==4.6.1"
+ },
+ "tomli": {
+ "hashes": [
+ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
+ "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==2.0.1"
+ },
+ "tomlkit": {
+ "hashes": [
+ "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b",
+ "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==0.12.4"
+ },
+ "typeguard": {
+ "hashes": [
+ "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4",
+ "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"
+ ],
+ "markers": "python_full_version >= '3.5.3'",
+ "version": "==2.13.3"
+ },
+ "typer": {
+ "hashes": [
+ "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2",
+ "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.9.0"
+ },
+ "types-awscrt": {
+ "hashes": [
+ "sha256:10245570c7285e949362b4ae710c54bf285d64a27453d42762477bcee5cd77a3",
+ "sha256:73be0a2720d6f76b924df6917d4edf4c9958f83e5c25bf7d9f0c1e9cdf836941"
+ ],
+ "markers": "python_version >= '3.7' and python_version < '4.0'",
+ "version": "==0.20.4"
+ },
+ "types-boto3": {
+ "hashes": [
+ "sha256:15f3ffad0314e40a0708fec25f94891414f93260202422bf8b19b6913853c983",
+ "sha256:a6a88e94d59d887839863a64095493956efc148e747206880a7eb47d90ae8398"
+ ],
+ "index": "pypi",
+ "version": "==1.0.2"
+ },
+ "types-pyyaml": {
+ "hashes": [
+ "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062",
+ "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"
+ ],
+ "index": "pypi",
+ "version": "==6.0.12.12"
+ },
+ "types-s3transfer": {
+ "hashes": [
+ "sha256:35e4998c25df7f8985ad69dedc8e4860e8af3b43b7615e940d53c00d413bdc69",
+ "sha256:44fcdf0097b924a9aab1ee4baa1179081a9559ca62a88c807e2b256893ce688f"
+ ],
+ "markers": "python_version >= '3.7' and python_version < '4.0'",
+ "version": "==0.10.0"
+ },
+ "types-setuptools": {
+ "hashes": [
+ "sha256:30a0d9903a81a424bd0f979534552a016a4543760aaffd499b9a2fe85bae0bfd",
+ "sha256:8a886a1fd06b668782dfbdaded4fd8a4e8c9f3d8d4c02acdd1240df098f50bf7"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==69.1.0.20240223"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
+ "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.10.0"
+ },
+ "typing-inspect": {
+ "hashes": [
+ "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f",
+ "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"
+ ],
+ "version": "==0.9.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
+ "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
+ ],
+ "markers": "python_version >= '3.10'",
+ "version": "==2.0.7"
+ },
+ "virtualenv": {
+ "hashes": [
+ "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a",
+ "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==20.25.1"
+ },
+ "werkzeug": {
+ "hashes": [
+ "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc",
+ "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.0.1"
+ },
+ "wheel": {
+ "hashes": [
+ "sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d",
+ "sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==0.42.0"
+ },
+ "xmltodict": {
+ "hashes": [
+ "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56",
+ "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"
+ ],
+ "markers": "python_version >= '3.4'",
+ "version": "==0.13.0"
+ },
+ "zipp": {
+ "hashes": [
+ "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31",
+ "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==3.17.0"
+ }
+ }
+}
diff --git a/source/modules/acdp/backstage/cdk/__init__.py b/source/modules/acdp/backstage/cdk/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/cdk.json b/source/modules/acdp/backstage/cdk/cdk.json
new file mode 100644
index 00000000..0aa94e8d
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/cdk.json
@@ -0,0 +1,48 @@
+{
+ "app": "python3 -m source.app",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "requirements*.txt",
+ "source.bat",
+ "**/__init__.py",
+ "python/__pycache__",
+ "tests"
+ ]
+ },
+ "context": {
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
+ "@aws-cdk/core:checkSecretUsage": true,
+ "@aws-cdk/core:target-partitions": [
+ "aws",
+ "aws-cn"
+ ],
+ "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
+ "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
+ "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
+ "@aws-cdk/aws-iam:minimizePolicies": true,
+ "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
+ "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
+ "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
+ "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
+ "@aws-cdk/core:enablePartitionLiterals": true,
+ "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
+ "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
+ "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
+ "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
+ "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
+ "@aws-cdk/aws-route53-patters:useCertificate": true,
+ "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
+ "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
+ "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
+ "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
+ "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
+ "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
+ "@aws-cdk/aws-redshift:columnId": true
+ }
+}
diff --git a/source/modules/acdp/backstage/cdk/deployment/build-s3-dist.sh b/source/modules/acdp/backstage/cdk/deployment/build-s3-dist.sh
new file mode 100755
index 00000000..75d83a6d
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/deployment/build-s3-dist.sh
@@ -0,0 +1,109 @@
+#!/bin/bash
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+
+showHelp() {
+cat << EOF
+Usage: ./deployment/build-s3-dist.sh --help
+
+Build and synthesize the CFN template. Package the templates into the deployment/global-s3-assets
+folder and the build assets in the deployment/regional-s3-assets folder.
+
+Example:
+./deployment/build-s3-dist.sh
+The template will then expect the build assets to be located in the solutions-features-[region_name] bucket.
+EOF
+}
+
+# Get reference for all important folders
+root_dir="$(dirname "$(dirname "$(realpath "$0")")")"
+deployment_dir="$root_dir/deployment"
+staging_dist_dir="$deployment_dir/staging"
+template_dist_dir="$deployment_dir/global-s3-assets"
+build_dist_dir="$deployment_dir/regional-s3-assets"
+
+printf "%b[VirtualEnv] Activating venv found in %s\n%b" "${GREEN}" "${root_dir}" "${NC}"
+source "$root_dir/.venv/bin/activate"
+
+printf "%b[Init] Remove old dist files from previous runs\n%b" "${GREEN}" "${NC}"
+rm -rf "$template_dist_dir"
+rm -rf "$build_dist_dir"
+rm -rf "$staging_dist_dir"
+
+mkdir -p "$template_dist_dir"
+mkdir -p "$build_dist_dir"
+mkdir -p "$staging_dist_dir"
+
+printf "%b[Init] Install dependencies for cdk-solution-helper\n%b" "${GREEN}" "${NC}"
+npm ci --prefix "$deployment_dir/cdk-solution-helper"
+
+printf "%b[Build] Build project specific assets\n%b" "${GREEN}" "${NC}"
+
+printf "%b[Synth] Synthesize Stack\n%b" "${GREEN}" "${NC}"
+cd "$root_dir"
+cdk synth --output="$staging_dist_dir" >> /dev/null
+
+printf "%b[Packing] Template artifacts\n%b" "${GREEN}" "${NC}"
+rm -f "$staging_dist_dir/tree.json"
+rm -f "$staging_dist_dir/manifest.json"
+rm -f "$staging_dist_dir/cdk.out"
+
+for f in "$staging_dist_dir"/*.template.json; do
+ mv "$f" "${f%.template.json}.template";
+ mv "${f%.template.json}.template" "$template_dist_dir";
+done
+
+cd "$deployment_dir/cdk-solution-helper"
+node index
+cd "$root_dir"
+
+printf "%b[Packing] Updating placeholders\n%b" "${GREEN}" "${NC}"
+sedi=(-i)
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ sedi=(-i "")
+fi
+
+for file in "$template_dist_dir"/*.template
+do
+ sed "${sedi[@]}" -E "s/\"\/([^asset][a-z0-9]+.zip)\"/\"\/asset\1\"/g" "$file"
+done
+
+printf "%b[Packing] Source code artifacts\n%b" "${GREEN}" "${NC}"
+# For each asset.*.zip source code artifact in the temporary /staging folder
+while IFS= read -r f; do
+ # Rename the artifact, removing the period for handler compatibility
+ zip_file_name="$(basename "$f")"
+ modified_zip_file_name="${zip_file_name/asset\./asset}"
+
+ # Copy the artifact from /staging to /regional-s3-assets
+ mv "$f" "$build_dist_dir/$modified_zip_file_name"
+done < <(find "$staging_dist_dir" -name "*.zip" -mindepth 1 -maxdepth 1 -type f)
+
+while IFS= read -r d; do
+ # Rename the artifact, removing the period for handler compatibility
+ dir_name="$(basename "$d")"
+ modified_dir_name="${dir_name/\./}"
+
+ # Zip artifacts from asset folder
+ cd "$d"
+ zip -r "$staging_dist_dir/$modified_dir_name.zip" . > /dev/null
+ cd "$root_dir"
+
+ # Copy the zipped artifact from /staging to /regional-s3-assets
+ mv "$staging_dist_dir/$modified_dir_name.zip" "$build_dist_dir"
+
+ # Remove the old artifacts from /staging
+ rm -rf "$d"
+done < <(find "$staging_dist_dir" -mindepth 1 -maxdepth 1 -type d)
+
+printf "%b[Cleanup] Remove temporary files\n%b" "${GREEN}" "${NC}"
+rm -rf "$staging_dist_dir"
+
+printf "%b[Move] Move assets into module specific asset directory\n%b" "${GREEN}" "${NC}"
+mkdir -p "$template_dist_dir/$MODULE_NAME"
+mkdir -p "$build_dist_dir/$MODULE_NAME"
+
+find "$template_dist_dir" -name "*.template" -maxdepth 1 -exec mv {} "$template_dist_dir/$MODULE_NAME/" \;
+find "$build_dist_dir" -name "*.zip" -maxdepth 1 -exec mv {} "$build_dist_dir/$MODULE_NAME/" \;
+
+printf "%bBuild script finished.\n%b" "${GREEN}" "${NC}"
diff --git a/source/modules/acdp/backstage/cdk/deployment/cdk-solution-helper/README.md b/source/modules/acdp/backstage/cdk/deployment/cdk-solution-helper/README.md
new file mode 100644
index 00000000..84a4e080
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/deployment/cdk-solution-helper/README.md
@@ -0,0 +1,159 @@
+# cdk-solution-helper
+
+A lightweight helper function that cleans-up synthesized templates from the AWS Cloud Development Kit (CDK) and prepares
+them for use with the AWS Solutions publishing pipeline. This function performs the following tasks:
+
+## Lambda function preparation
+
+Replaces the AssetParameter-style properties that identify source code for Lambda functions with the common variables
+used by the AWS Solutions publishing pipeline.
+
+- `Code.S3Bucket` is assigned the `%%DIST_BUCKET_NAME%%` placeholder value.
+- `Code.S3Key` is assigned the `%%SOLUTION_NAME%%`/`%%VERSION%%` placeholder value.
+- `Handler` is given a prefix identical to the artifact hash, enabling the Lambda function to properly find the handler
+in the extracted source code package.
+
+These placeholders are then replaced with the appropriate values using the default find/replace operation run by the pipeline.
+
+Before:
+
+```json
+"examplefunction67F55935": {
+ "Type": "AWS::Lambda::Function",
+ "Properties": {
+ "Code": {
+ "S3Bucket": {
+ "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3Bucket54E71A95"
+ },
+ "S3Key": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Fn::Select": [
+ 0,
+ {
+ "Fn::Split": [
+ "||",
+ {
+ "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Fn::Select": [
+ 1,
+ {
+ "Fn::Split": [
+ "||",
+ {
+ "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]
+ }
+ }, ...
+ Handler: "index.handler", ...
+```
+
+After helper function run:
+
+```json
+"examplefunction67F55935": {
+ "Type": "AWS::Lambda::Function",
+ "Properties": {
+ "Code": {
+ "S3Bucket": "%%DIST_BUCKET_NAME%%",
+ "S3Key": "%%SOLUTION_NAME%%/%%VERSION%%/assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip"
+ }, ...
+ "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler"
+```
+
+After build script run:
+
+```json
+"examplefunction67F55935": {
+ "Type": "AWS::Lambda::Function",
+ "Properties": {
+ "Code": {
+ "S3Bucket": "solutions",
+ "S3Key": "trademarked-solution-name/v1.0.0/asset.d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip"
+ }, ...
+ "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler"
+```
+
+After CloudFormation deployment:
+
+```json
+"examplefunction67F55935": {
+ "Type": "AWS::Lambda::Function",
+ "Properties": {
+ "Code": {
+ "S3Bucket": "solutions-us-east-1",
+ "S3Key": "trademarked-solution-name/v1.0.0/asset.d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip"
+ }, ...
+ "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler"
+```
+
+## Template cleanup
+
+Cleans-up the parameters section and improves readability by removing the AssetParameter-style fields that would have
+been used to specify Lambda source code properties. This allows solution-specific parameters to be highlighted and
+removes unnecessary clutter.
+
+Before:
+
+```json
+"Parameters": {
+ "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3Bucket54E71A95": {
+ "Type": "String",
+ "Description": "S3 bucket for asset \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\""
+ },
+ "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1": {
+ "Type": "String",
+ "Description": "S3 key for asset version \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\""
+ },
+ "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7ArtifactHash7AA751FE": {
+ "Type": "String",
+ "Description": "Artifact hash for asset \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\""
+ },
+ "CorsEnabled" : {
+ "Description" : "Would you like to enable Cross-Origin Resource Sharing (CORS) for the image handler API? Select 'Yes' if so.",
+ "Default" : "No",
+ "Type" : "String",
+ "AllowedValues" : [ "Yes", "No" ]
+ },
+ "CorsOrigin" : {
+ "Description" : "If you selected 'Yes' above, please specify an origin value here. A wildcard (*) value will support any origin.",
+ "Default" : "*",
+ "Type" : "String"
+ }
+ }
+```
+
+After:
+
+```json
+"Parameters": {
+ "CorsEnabled" : {
+ "Description" : "Would you like to enable Cross-Origin Resource Sharing (CORS) for the image handler API? Select 'Yes' if so.",
+ "Default" : "No",
+ "Type" : "String",
+ "AllowedValues" : [ "Yes", "No" ]
+ },
+ "CorsOrigin" : {
+ "Description" : "If you selected 'Yes' above, please specify an origin value here. A wildcard (*) value will support any origin.",
+ "Default" : "*",
+ "Type" : "String"
+ }
+ }
+ ```
+
+***
+© Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
diff --git a/source/modules/acdp/backstage/cdk/deployment/cdk-solution-helper/index.js b/source/modules/acdp/backstage/cdk/deployment/cdk-solution-helper/index.js
new file mode 100644
index 00000000..61df8783
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/deployment/cdk-solution-helper/index.js
@@ -0,0 +1,331 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// Imports
+const fs = require("fs");
+
+// Paths
+const globalS3AssetsPath = "../global-s3-assets";
+
+// Substitution constants and functions
+
+// Specific to backstage cdk-helper only,
+// This runs directly in an account w/ built assets directly created in a pipeline,
+// so there is no region prefix for assets.
+const regionalS3AssetsBucketSub = {
+ "Fn::FindInMap": ["Solution", "AssetsConfig", "S3AssetBucketName"],
+};
+
+function regionalS3AssetsKeySub(assetPath) {
+ return {
+ "Fn::Join": [
+ "/",
+ [
+ {
+ "Fn::FindInMap": ["Solution", "AssetsConfig", "S3AssetKeyPrefix"],
+ },
+ `${assetPath}`,
+ ],
+ ],
+ };
+}
+
+function substituteLambdaAssets(template, resources) {
+ // Clean-up Lambda function code dependencies
+ const lambdaFunctions = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::Lambda::Function";
+ });
+ lambdaFunctions.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+ if (fn.Properties.hasOwnProperty("Code")) {
+ prop = fn.Properties.Code;
+ } else if (fn.Properties.hasOwnProperty("Content")) {
+ prop = fn.Properties.Content;
+ }
+
+ if (prop.hasOwnProperty("S3Bucket")) {
+ // Set the S3 key reference
+ let artifactHash = Object.assign(prop.S3Key);
+ const assetPath = `asset${artifactHash}`;
+ prop.S3Key = regionalS3AssetsKeySub(assetPath);
+
+ // Set the S3 bucket reference
+ prop.S3Bucket = regionalS3AssetsBucketSub;
+ } else {
+ console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
+ }
+ });
+}
+
+function substituteLambdaLayerAssets(template, resources) {
+ const lambdaLayers = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::Lambda::LayerVersion";
+ });
+ lambdaLayers.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+ if (fn.Properties.hasOwnProperty("Content")) {
+ prop = fn.Properties.Content;
+ }
+
+ if (prop.hasOwnProperty("S3Bucket")) {
+ // Set the S3 key reference
+ let artifactHash = Object.assign(prop.S3Key);
+ const assetPath = `asset${artifactHash}`;
+ prop.S3Key = regionalS3AssetsKeySub(assetPath);
+
+ // Set the S3 bucket reference
+ prop.S3Bucket = regionalS3AssetsBucketSub;
+ } else {
+ console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
+ }
+ });
+}
+
+function substituteServerlessFunctionAssets(template, resources) {
+ const serverlessFunctions = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::Serverless::Function";
+ });
+ serverlessFunctions.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+ if (fn.Properties.hasOwnProperty("CodeUri")) {
+ prop = fn.Properties.CodeUri;
+ }
+
+ if (prop.hasOwnProperty("Bucket")) {
+ // Set the S3 key reference
+ let artifactHash = Object.assign(prop.Key);
+ const assetPath = `asset${artifactHash}`;
+ prop.Key = regionalS3AssetsKeySub(assetPath);
+
+ // Set the S3 bucket reference
+ prop.Bucket = regionalS3AssetsBucketSub;
+ } else {
+ console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
+ }
+ });
+}
+
+function substituteCDKBucketDeploymentAssets(template, resources) {
+ const cdkBucketDeployments = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "Custom::CDKBucketDeployment";
+ });
+ cdkBucketDeployments.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop = fn.Properties;
+
+ if (prop.hasOwnProperty("SourceBucketNames")) {
+ // Set the S3 key reference
+ let artifactHash = Object.assign(prop.SourceObjectKeys);
+ const assetPath = `asset${artifactHash}`;
+ prop.SourceObjectKeys = [regionalS3AssetsKeySub(assetPath)];
+
+ // Set the S3 bucket reference
+ prop.SourceBucketNames = [regionalS3AssetsBucketSub];
+ } else {
+ console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
+ }
+ });
+}
+
+function substituteCodeCommitRepoAssets(template, resources) {
+ const codeCommitRepos = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::CodeCommit::Repository";
+ });
+ codeCommitRepos.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+
+ if (fn.Properties.hasOwnProperty("Code")) {
+ prop = fn.Properties.Code;
+ }
+
+ if (prop.hasOwnProperty("S3")) {
+ prop = prop.S3;
+ // Set the S3 key reference
+ let artifactHash = Object.assign(prop.Key);
+ const assetPath = `asset${artifactHash}`;
+ prop.Key = regionalS3AssetsKeySub(assetPath);
+ // Set the S3 bucket reference
+
+ prop.Bucket = regionalS3AssetsBucketSub;
+ } else {
+ console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
+ }
+ });
+}
+
+function substituteCodePipelineAssets(template, resources) {
+ const codePipelines = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::CodePipeline::Pipeline";
+ });
+ codePipelines.forEach(function (f) {
+ const fn = template.Resources[f];
+ let stages;
+
+ if (fn.Properties.hasOwnProperty("Stages")) {
+ stages = fn.Properties.Stages;
+ }
+
+ stages.forEach(function (s) {
+ let actions = s.Actions;
+ actions.forEach(function (a) {
+ if (
+ a.ActionTypeId.Category == "Source" &&
+ a.ActionTypeId.Provider == "S3"
+ ) {
+ a.Configuration.S3Bucket = regionalS3AssetsBucketSub;
+ }
+ });
+ });
+ });
+}
+
+function substituteNestedStackAssets(template, resources) {
+ // Clean-up nested template stack dependencies
+ const nestedStacks = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::CloudFormation::Stack";
+ });
+
+ nestedStacks.forEach(function (f) {
+ const fn = template.Resources[f];
+ let assetPath = fn.Metadata["aws:asset:path"];
+ // get the base name of the asset path file. Trim the .json at the end
+ if (
+ assetPath.substring(assetPath.length - 5, assetPath.length) === ".json"
+ ) {
+ assetPath = assetPath.substring(0, assetPath.length - 5);
+ }
+
+ fn.Properties.TemplateURL = {
+ "Fn::Join": [
+ "",
+ [
+ "https://",
+ regionalS3AssetsBucketSub,
+ ".s3.",
+ {
+ Ref: "AWS::URLSuffix",
+ },
+ "/",
+ regionalS3AssetsKeySub(assetPath),
+ ],
+ ],
+ };
+ });
+}
+
+function compareJsonKeys(json1, json2) {
+ if (typeof json1 !== "object" || typeof json2 !== "object") {
+ return false;
+ }
+ let keys1 = Object.keys(json1).sort();
+ let keys2 = Object.keys(json2).sort();
+
+ return JSON.stringify(keys1) === JSON.stringify(keys2);
+}
+
+function compareJsonsWithRegex(jsonWithPattern, jsonToMatch) {
+ if (
+ typeof jsonWithPattern !== "object" ||
+ typeof jsonToMatch !== "object" ||
+ !compareJsonKeys(jsonWithPattern, jsonToMatch)
+ ) {
+ return false;
+ }
+
+ for (const key in jsonWithPattern) {
+ var re = new RegExp(`^${jsonWithPattern[key]}$`);
+ if (!re.test(jsonToMatch[key])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function replaceSubdict(jsonObj, targetSubdict, replacement) {
+ for (const key in jsonObj) {
+ if (jsonObj[key] && typeof jsonObj[key] === "object") {
+ if (compareJsonsWithRegex(targetSubdict, jsonObj[key])) {
+ jsonObj[key] = { ...replacement };
+ } else {
+ replaceSubdict(jsonObj[key], targetSubdict, replacement);
+ }
+ }
+ }
+}
+
+function substitutePolicies(template, resources) {
+ const policies = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::IAM::Policy";
+ });
+ policies.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+ if (fn.Properties.hasOwnProperty("PolicyDocument")) {
+ prop = fn.Properties.PolicyDocument;
+ }
+
+ let cdkBucketRef = {
+ "Fn::Sub": "cdk-[a-z0-9]+-assets-.*",
+ };
+
+ let customBucketRef = regionalS3AssetsBucketSub;
+
+ replaceSubdict(prop, cdkBucketRef, customBucketRef);
+ });
+}
+
+function substituteRoles(template, resources) {
+ const roles = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::IAM::Role";
+ });
+ roles.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+ if (fn.Properties.hasOwnProperty("Policies")) {
+ prop = fn.Properties.Policies;
+ }
+
+ let cdkBucketRef = {
+ "Fn::Sub": "cdk-[a-z0-9]+-assets-.*",
+ };
+ let customBucketRef = regionalS3AssetsBucketSub;
+
+ replaceSubdict(prop, cdkBucketRef, customBucketRef);
+ });
+}
+
+// For each template in globalS3AssetsPath ...
+fs.readdirSync(globalS3AssetsPath).forEach((file) => {
+ // Import and parse template file
+ const rawTemplate = fs.readFileSync(`${globalS3AssetsPath}/${file}`);
+ let template = JSON.parse(rawTemplate);
+ const resources = template.Resources ? template.Resources : {};
+
+ substituteLambdaAssets(template, resources);
+ substituteLambdaLayerAssets(template, resources);
+ substituteServerlessFunctionAssets(template, resources);
+ substituteCDKBucketDeploymentAssets(template, resources);
+ substituteCodeCommitRepoAssets(template, resources);
+ substituteNestedStackAssets(template, resources);
+ substituteCodePipelineAssets(template, resources);
+ substitutePolicies(template, resources);
+ substituteRoles(template, resources);
+
+ // Clean-up parameters section
+ const parameters = template.Parameters ? template.Parameters : {};
+ const assetParameters = Object.keys(parameters).filter(function (key) {
+ return key.includes("AssetParameters");
+ });
+ assetParameters.forEach(function (a) {
+ template.Parameters[a] = undefined;
+ });
+
+ // Output modified template file
+ const outputTemplate = JSON.stringify(template, null, 2);
+ fs.writeFileSync(`${globalS3AssetsPath}/${file}`, outputTemplate);
+});
diff --git a/deployment/cdk-solution-helper/package.json b/source/modules/acdp/backstage/cdk/deployment/cdk-solution-helper/package.json
similarity index 100%
rename from deployment/cdk-solution-helper/package.json
rename to source/modules/acdp/backstage/cdk/deployment/cdk-solution-helper/package.json
diff --git a/source/modules/acdp/backstage/cdk/deployment/determine-bucket-region.sh b/source/modules/acdp/backstage/cdk/deployment/determine-bucket-region.sh
new file mode 100755
index 00000000..d9afd814
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/deployment/determine-bucket-region.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+cache_file="${TMPDIR:-/tmp/}${BUCKET}"
+[ -f "$cache_file" ] && cat "$cache_file" && exit 0
+
+url="https://${BUCKET}.s3.amazonaws.com"
+status_code=$(curl -s -o /dev/null -w "%{http_code}" -I "$url")
+
+if [ "$status_code" -eq 404 ]; then
+ bucket_region=${AWS_REGION};
+elif [ "$status_code" -eq 200 ] || [ "$status_code" -eq 401 ] || [ "$status_code" -eq 403 ]; then
+ bucket_region=$(curl -sI "$url" | grep x-amz-bucket-region | awk '{print $2}' | tr -d '\r');
+ if [ -z "$bucket_region" ]; then
+ bucket_region=${AWS_REGION};
+ fi
+fi
+
+echo "$bucket_region" > "$cache_file"
+# Print the bucket region
+echo "$bucket_region"
diff --git a/source/modules/acdp/backstage/cdk/deployment/run-cfn-nag.sh b/source/modules/acdp/backstage/cdk/deployment/run-cfn-nag.sh
new file mode 100644
index 00000000..5686ee5d
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/deployment/run-cfn-nag.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+
+showHelp() {
+cat << EOF
+Usage: ./deployment/run-cfn-nag.sh --help
+
+Run "cdk-nag" and cfn-nag in this module.
+
+-dl, --deny-list-path Pass the file name which contains cfn-nag rules to suppress
+
+EOF
+}
+
+while [[ $# -gt 0 ]]
+do
+key="$1"
+case $key in
+ -h|--help)
+ showHelp
+ exit 0
+ ;;
+ -dl|--deny-list-path)
+ deny_list_path="$2"
+ shift
+ shift
+ ;;
+ *)
+ shift
+esac
+done
+
+# CD into one level above the deployment dir where this script is located
+cd "$(dirname "$0")"/..
+
+# Get reference for all important folders
+root_dir="$(dirname "$(dirname "$(realpath "$0")")")"
+deployment_dir="$root_dir/deployment"
+template_dist_dir="$deployment_dir/global-s3-assets"
+
+# Run the build script to build the assets and templates
+printf "%bBuild the assets for the module.%b\n" "${MAGENTA}" "${NC}"
+export CDK_NAG_ENFORCE=true
+make -C "$root_dir" build
+
+did_cfn_nag_fail=0
+# Loop through all files with extension .template in the template_dist_dir
+while IFS= read -r file; do
+ # Check if the file exists and is a file (not a directory)
+ if [[ -f "${file}" ]]; then
+ # Fail if exit code is non-0. The if statement is necessary to prevent exit because of `set -e`.
+ if ! output=$(cfn_nag "${file}" ${deny_list_path:+--deny-list-path=$deny_list_path} 2>&1); then
+ did_cfn_nag_fail=1
+ printf "%bCFN NAG scan failed with failures.%b\n" "${RED}" "${NC}"
+ fi
+ # Check if there are any warnings in the output. cfn_nag does not return a failing exit code on warnings.
+ if [[ "${output}" == *"WARN"* ]]; then
+ did_cfn_nag_fail=1
+ printf "%bCFN NAG scan failed with warnings.%b\n" "${RED}" "${NC}"
+ fi
+ echo "$output"
+ fi
+done < <(find "$template_dist_dir" -name "*.template" -mindepth 1 -type f)
+
+exit $did_cfn_nag_fail
diff --git a/source/modules/acdp/backstage/cdk/deployment/run-unit-tests.sh b/source/modules/acdp/backstage/cdk/deployment/run-unit-tests.sh
new file mode 100644
index 00000000..05c1baf0
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/deployment/run-unit-tests.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+# NOT IMPLEMENTED
+exit
diff --git a/source/modules/acdp/backstage/cdk/deployment/upload-s3-dist.sh b/source/modules/acdp/backstage/cdk/deployment/upload-s3-dist.sh
new file mode 100755
index 00000000..017b4df6
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/deployment/upload-s3-dist.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+#
+# This script will perform the following tasks:
+# 1. Creates the template and build assets bucket.
+# 2. Copy the contents of the global-s3-assets/ directory to the template bucket
+# 3. Copy the contents of the regional-s3-assets/ directory to the build assets bucket
+#
+# Usage
+# ./deployment/upload-s3-dist.sh
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+shopt -s nullglob
+
+[[ -z "$AWS_ACCOUNT_ID" ]] && printf "%bUnable to identify AWS_ACCOUNT_ID, please add AWS_ACCOUNT_ID to environment variables%b\n" "${RED}" "${NC}" && exit 1
+[[ -z "$AWS_REGION" ]] && printf "%bUnable to identify AWS_REGION, please add AWS_REGION to environment variables%b\n" "${RED}" "${NC}" && exit 1
+[[ -z "$GLOBAL_ASSET_BUCKET_NAME" ]] && printf "%bUnable to identify GLOBAL_ASSET_BUCKET_NAME, please add GLOBAL_ASSET_BUCKET_NAME to environment variables%b\n" "${RED}" "${NC}" && exit 1
+[[ -z "$REGIONAL_ASSET_BUCKET_NAME" ]] && printf "%bUnable to identify REGIONAL_ASSET_BUCKET_NAME, please add REGIONAL_ASSET_BUCKET_NAME to environment variables%b\n" "${RED}" "${NC}" && exit 1
+
+template_dist_dir="$PWD/deployment/global-s3-assets"
+build_dist_dir="$PWD/deployment/regional-s3-assets"
+s3_key_prefix="$SOLUTION_NAME/$SOLUTION_VERSION"
+
+printf "%bCopying template files from %s to %s bucket...%b\n" "${MAGENTA}" "$template_dist_dir" "$GLOBAL_ASSET_BUCKET_NAME" "${NC}"
+while IFS= read -r -d '' template_file_path; do
+ relative_template_file_path=${template_file_path/$template_dist_dir/}
+ printf "%s\n" "Template: $relative_template_file_path"
+
+ s3_key="$s3_key_prefix$relative_template_file_path"
+ aws s3api put-object \
+ --bucket "$GLOBAL_ASSET_BUCKET_NAME" \
+ --key "$s3_key" \
+ --body "$template_file_path" \
+ --expected-bucket-owner "$AWS_ACCOUNT_ID" > /dev/null
+done < <(find "$template_dist_dir" -name "*.template" -type f -print0)
+
+# this doesn't handle directories, needs to be improved
+printf "%bCopying build asset files from %s to %s bucket...%b\n" "${MAGENTA}" "$build_dist_dir" "$REGIONAL_ASSET_BUCKET_NAME" "${NC}"
+
+while IFS= read -r -d '' asset_file_path; do
+ relative_asset_file_path="${asset_file_path/$build_dist_dir/}"
+ printf "%s\n" "Asset: $relative_asset_file_path"
+
+ s3_key="$s3_key_prefix$relative_asset_file_path"
+ aws s3api put-object \
+ --bucket "$REGIONAL_ASSET_BUCKET_NAME" \
+ --key "$s3_key" \
+ --body "$asset_file_path" \
+ --expected-bucket-owner "$AWS_ACCOUNT_ID" > /dev/null
+done < <(find "$build_dist_dir" -type f -print0)
diff --git a/source/modules/acdp/backstage/cdk/setup.py b/source/modules/acdp/backstage/cdk/setup.py
new file mode 100644
index 00000000..f32ed094
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/setup.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import os
+
+# Third Party Libraries
+import setuptools
+
+try:
+ with open("README.md", "r", encoding="utf-8") as fp:
+ LONG_DESCRIPTION = fp.read()
+except FileNotFoundError:
+ LONG_DESCRIPTION = ""
+
+setuptools.setup(
+ name=os.environ["MODULE_NAME"],
+ version=setuptools.sic(os.environ["MODULE_VERSION"]),
+ description=os.environ["MODULE_DESCRIPTION"],
+ long_description=LONG_DESCRIPTION,
+ long_description_content_type="text/markdown",
+ author=os.environ["MODULE_AUTHOR"],
+ python_requires=f">={os.environ['PYTHON_MINIMUM_VERSION_SUPPORTED']}",
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: JavaScript",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Typing :: Typed",
+ ],
+)
diff --git a/source/modules/acdp/backstage/cdk/source/.cdk-nag-suppression-list.json b/source/modules/acdp/backstage/cdk/source/.cdk-nag-suppression-list.json
new file mode 100644
index 00000000..fd629a7d
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/.cdk-nag-suppression-list.json
@@ -0,0 +1,106 @@
+{
+ "/acdp-backstage/backstage/backstage-elb-logs-bucket/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-S1",
+ "reason": "An logs bucket does not need S3 bucket for access logs"
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/backstage-cognito-user-pool/backstage-user-pool-domain/CloudFrontDomainName/CustomResourcePolicy/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "reason": "Wildcard for SAN is only for subdomains of provided Host Zone name"
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/backstage-task-definition-role/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "reason": "Wildcards are restricted and prefixed where possible to limit their scope"
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/backstage-task-definition-role/DefaultPolicy/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "reason": "Default policy here is least privilege"
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/backstage-ecs-fargate-task-definition/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-ECS2",
+ "reason": "All environment variables defined this way are not sensitive information."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/acdp-backstage-alb/SecurityGroup/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-EC23",
+ "reason": "This is expected as it is accessed from the web and auth is in place to prevent unintended users"
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/cognito-certificate/CertificateRequestorFunction/ServiceRole/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM4",
+ "reason": "Default policy here is least privilege"
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/cognito-certificate/CertificateRequestorFunction/ServiceRole/DefaultPolicy/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "reason": "Default policy here is least privilege"
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/alb-listener-certificate/CertificateRequestorFunction/ServiceRole/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM4",
+ "reason": "Default policy here is least privilege"
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/alb-listener-certificate/CertificateRequestorFunction/ServiceRole/DefaultPolicy/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "reason": "Default policy here is least privilege"
+ }
+ ]
+ },
+ "/acdp-backstage/AWS679f53fac002430cb0da5b7982bd2287/ServiceRole/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM4",
+ "reason": "Default policy here is least privilege"
+ }
+ ]
+ },
+ "/backstage-env-dev/backstage-env/backstage-aurora-postgres/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-RDS6",
+ "reason": "IAM Database Authentication is not enabled by default and can easily be added in"
+ },
+ {
+ "id": "AwsSolutions-RDS11",
+ "reason": "The default endpoint port is expected to be used here"
+ },
+ {
+ "id": "AwsSolutions-RDS10",
+ "reason": "Delete protection disabled intentionally. Preference is to use backup and restore capabilities."
+ }
+ ]
+ }
+}
diff --git a/source/modules/acdp/backstage/cdk/source/.cfn-nag-suppression-list.json b/source/modules/acdp/backstage/cdk/source/.cfn-nag-suppression-list.json
new file mode 100644
index 00000000..c7741e81
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/.cfn-nag-suppression-list.json
@@ -0,0 +1,166 @@
+{
+ "/acdp-backstage/backstage/backstage-cognito-user-pool/backstage-user-pool-domain/CloudFrontDomainName/CustomResourcePolicy/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W12",
+ "reason": "Resource wildcard for cognito-idp:DescribeUserPoolDomain is required."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/backstage-task-definition-role/DefaultPolicy/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W12",
+ "reason": "Resource wildcard for ecr:GetAuthorizationToken is required."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/cognito-certificate/CertificateRequestorFunction/ServiceRole/DefaultPolicy/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W12",
+ "reason": "Resource wildcard for ACM actions is required."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/alb-listener-certificate/CertificateRequestorFunction/ServiceRole/DefaultPolicy/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W12",
+ "reason": "Resource wildcard for ACM actions is required."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/backstage-task-definition-role/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W11",
+ "reason": "Resource wildcards are required on S3 buckets"
+ },
+ {
+ "id": "W28",
+ "reason": "Explicit name is accepted for this resource."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/cognito-certificate/CertificateRequestorFunction/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W58",
+ "reason": "Automatically created lambda by Lambda Function construct, does not need log permissions."
+ },
+ {
+ "id": "W89",
+ "reason": "VPC not required for this project for now."
+ },
+ {
+ "id": "W92",
+ "reason": "Reserved concurrent executions not required for now."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/alb-listener-certificate/CertificateRequestorFunction/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W58",
+ "reason": "Automatically created lambda by Lambda Function construct, does not need log permissions."
+ },
+ {
+ "id": "W89",
+ "reason": "VPC not required for this project for now."
+ },
+ {
+ "id": "W92",
+ "reason": "Reserved concurrent executions not required for now."
+ }
+ ]
+ },
+ "/acdp-backstage/AWS679f53fac002430cb0da5b7982bd2287/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W58",
+ "reason": "Automatically created lambda by Lambda Function construct, does not need log permissions."
+ },
+ {
+ "id": "W89",
+ "reason": "VPC not required for this project for now."
+ },
+ {
+ "id": "W92",
+ "reason": "Reserved concurrent executions not required for now."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/acdp-backstage-alb/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W28",
+ "reason": "Explicit name is accepted for this resource."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/backstage-elb-logs-bucket/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W35",
+ "reason": "Server access logs bucket does not need logging configured as it is a log bucket itself."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/backstage-ecs-fargate-service/SecurityGroup/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W40",
+ "reason": "This is expected as it is accessed from the web and auth is in place to prevent unintended users."
+ },
+ {
+ "id": "W5",
+ "reason": "This is expected as it is accessed from the web and auth is in place to prevent unintended users."
+ }
+ ]
+ },
+ "/acdp-backstage/backstage/acdp-backstage-alb/SecurityGroup/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W9",
+ "reason": "This is expected as it is accessed from the web and auth is in place to prevent unintended users."
+ },
+ {
+ "id": "W2",
+ "reason": "This is expected as it is accessed from the web and auth is in place to prevent unintended users."
+ }
+ ]
+ },
+ "/backstage-env-dev/backstage-env/backstage-aurora-postgres/Secret/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W77",
+ "reason": "AWS managed KMS key is sufficient for SecretsManager Secret."
+ }
+ ]
+ },
+ "/backstage-env-dev/backstage-env/backstage-database-security-group/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W40",
+ "reason": "This is expected as it is accessed from the web and auth is in place to prevent unintended users."
+ },
+ {
+ "id": "W5",
+ "reason": "This is expected as it is accessed from the web and auth is in place to prevent unintended users."
+ }
+ ]
+ },
+ "/backstage-env-dev/backstage-env/backstage-aurora-postgres/RotationSingleUser/SecurityGroup/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "W40",
+ "reason": "This is set by password rotation cdk feature .add_rotation_single_user(...)."
+ },
+ {
+ "id": "W5",
+ "reason": "This is set by password rotation cdk feature .add_rotation_single_user(...)."
+ }
+ ]
+ }
+}
diff --git a/source/modules/acdp/backstage/cdk/source/__init__.py b/source/modules/acdp/backstage/cdk/source/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/app.py b/source/modules/acdp/backstage/cdk/source/app.py
new file mode 100644
index 00000000..a9b4bb53
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/app.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import os
+from os.path import dirname, realpath
+
+# AWS Libraries
+from aws_cdk import App, Aspects, DefaultStackSynthesizer, Environment
+from cdk_nag import AwsSolutionsChecks
+
+# Connected Mobility Solution on AWS
+from .infrastructure.acdp_backstage_stack import AcdpBackstageStack
+from .infrastructure.aspects.backstage_nag_suppression import NagSuppression, NagType
+from .infrastructure.lib.cms_common.aspects.vpc_aspect import ApplyVpcOnCustomResource
+from .infrastructure.lib.cms_common.config.stack_inputs import (
+ S3AssetConfigInputs,
+ SolutionConfigInputs,
+ create_solution_tags_for_stack,
+ create_stack_description,
+)
+
+solution_config_inputs = SolutionConfigInputs(
+ solution_id=os.environ["SOLUTION_ID"],
+ solution_name=os.environ["SOLUTION_NAME"],
+ solution_version=os.environ["SOLUTION_VERSION"],
+ application_type=os.environ["APPLICATION_TYPE"],
+ module_name=os.environ["MODULE_NAME"],
+ module_short_name=os.environ["MODULE_SHORT_NAME"],
+ capability_id=os.environ["CAPABILITY_ID"],
+)
+
+# The bucket_base_name is unused and instead replaced by s3_asset_bucket_name in backstage because
+# cdk synth occurs in the deployment account to a local acdp owned bucket
+s3_asset_config_inputs = S3AssetConfigInputs(
+ bucket_base_name="UNUSED",
+ object_key_prefix=os.environ["S3_ASSET_KEY_PREFIX"],
+)
+s3_asset_bucket_name = os.environ["LOCAL_ASSET_BUCKET_NAME"]
+
+app = App()
+
+backstage_stack = AcdpBackstageStack(
+ app,
+ solution_config_inputs.module_name,
+ description=create_stack_description(solution_config=solution_config_inputs),
+ solution_config_inputs=solution_config_inputs,
+ s3_asset_config_inputs=s3_asset_config_inputs,
+ s3_asset_bucket_name=s3_asset_bucket_name,
+ env=Environment(
+ account=os.environ["AWS_ACCOUNT_ID"], region=os.environ["AWS_REGION"]
+ ),
+ synthesizer=DefaultStackSynthesizer(generate_bootstrap_version_rule=False),
+)
+
+create_solution_tags_for_stack(app, solution_config=solution_config_inputs)
+
+# Aspects
+Aspects.of(app).add(
+ ApplyVpcOnCustomResource(
+ module_name=solution_config_inputs.module_name,
+ security_group_logical_ids=backstage_stack.backstage_construct.cdk_lambdas_vpc_construct.security_groups,
+ subnet_names=backstage_stack.backstage_construct.cdk_lambdas_vpc_construct.subnets,
+ )
+)
+
+
+Aspects.of(app).add(
+ NagSuppression(
+ f"{dirname(realpath(__file__))}/.cdk-nag-suppression-list.json", NagType.CDK_NAG
+ )
+)
+Aspects.of(app).add(
+ NagSuppression(
+ f"{dirname(realpath(__file__))}/.cfn-nag-suppression-list.json", NagType.CFN_NAG
+ )
+)
+if os.environ.get("CDK_NAG_ENFORCE") == "true":
+ Aspects.of(app).add(AwsSolutionsChecks())
+
+app.synth()
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/__init__.py b/source/modules/acdp/backstage/cdk/source/infrastructure/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/acdp_backstage_stack.py b/source/modules/acdp/backstage/cdk/source/infrastructure/acdp_backstage_stack.py
new file mode 100644
index 00000000..c5f905a2
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/acdp_backstage_stack.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import os
+from typing import Any, List
+
+# AWS Libraries
+from aws_cdk import CfnMapping, Duration, Stack, Tags, aws_rds, aws_ssm
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from .constructs.aurora_database import AuroraDatabaseConstruct
+from .constructs.backstage_container import BackstageContainerConstruct
+from .constructs.cognito import CognitoConstruct
+from .constructs.load_balancer import LoadBalancerConstruct
+from .constructs.module_integration import ModuleInputsConstruct
+from .constructs.route53 import Route53Construct
+from .lib.cms_common.config.resource_names import ResourceName
+from .lib.cms_common.config.stack_inputs import (
+ S3AssetConfigInputs,
+ SolutionConfigInputs,
+)
+from .lib.cms_common.constructs.app_unique_id import AppUniqueId
+from .lib.cms_common.constructs.cdk_lambda_vpc_config_construct import (
+ CDKLambdasVpcConfigConstruct,
+)
+from .lib.cms_common.constructs.vpc_construct import VpcConstruct
+
+
+class AcdpBackstageStack(Stack):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ solution_config_inputs: SolutionConfigInputs,
+ s3_asset_config_inputs: S3AssetConfigInputs,
+ s3_asset_bucket_name: str,
+ *args: List[Any],
+ **kwargs: Any,
+ ) -> None:
+ super().__init__(scope, construct_id, *args, **kwargs)
+
+ CfnMapping(
+ self,
+ "Solution",
+ mapping={
+ "AssetsConfig": {
+ "S3AssetBucketName": s3_asset_bucket_name,
+ "S3AssetKeyPrefix": s3_asset_config_inputs.object_key_prefix,
+ },
+ },
+ )
+
+ module_inputs = ModuleInputsConstruct(
+ self,
+ "module-inputs-construct",
+ solution_config_inputs=solution_config_inputs,
+ )
+
+ # Check if a config stack for the app unique id is registered. Fail stack
+ # creation if it is not registered. If config stack exists, then create an SSM
+ # parameter to register the module with the app unique id.
+ register_module_with_app_unique_id = AppUniqueId.register_module(
+ self,
+ app_unique_id=module_inputs.acdp_uid,
+ module_name=solution_config_inputs.module_short_name,
+ )
+
+ deployment_uuid = aws_ssm.StringParameter.from_string_parameter_attributes(
+ self,
+ "deployment-uuid",
+ parameter_name=ResourceName.slash_separated(
+ prefix=module_inputs.acdp_config_ssm_prefix_with_slash_prefix,
+ name="deployment-uuid",
+ ),
+ simple_name=True,
+ force_dynamic_reference=True,
+ ).string_value
+
+ self.backstage_construct = AcdpBackstageConstruct(
+ self, "acdp-backstage", module_inputs=module_inputs
+ )
+ self.backstage_construct.node.add_dependency(register_module_with_app_unique_id)
+
+ Tags.of(self.backstage_construct).add(
+ "Solutions:DeploymentUUID", deployment_uuid
+ )
+
+
+class AcdpBackstageConstruct(Construct):
+ def __init__( # pylint: disable=R0914
+ self,
+ scope: Construct,
+ construct_id: str,
+ module_inputs: ModuleInputsConstruct,
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ vpc_construct = VpcConstruct(
+ self, "vpc-construct", vpc_config=module_inputs.vpc_config
+ )
+
+ self.cdk_lambdas_vpc_construct = CDKLambdasVpcConfigConstruct(
+ self,
+ "cdk-lambdas-vpc-construct",
+ vpc_construct=vpc_construct,
+ subnets=module_inputs.vpc_config.private_subnets,
+ )
+
+ # Workaround for issue w/ SSM tokenization failing on lookup of IHostedZone
+ route53_hosted_zone_name = os.environ["ROUTE53_HOSTED_ZONE_NAME"]
+ route53_base_domain = os.environ["ROUTE53_BASE_DOMAIN"]
+
+ route53_construct = Route53Construct(
+ self,
+ "route53-construct",
+ route53_hosted_zone_name=route53_hosted_zone_name,
+ route53_base_domain=route53_base_domain,
+ )
+
+ aurora_database_construct = AuroraDatabaseConstruct(
+ self,
+ "aurora-database-construct",
+ vpc=vpc_construct.vpc, # type: ignore[arg-type]
+ isolated_subnets=vpc_construct.isolated_subnet_selection,
+ credentials_secret_name=f"{module_inputs.acdp_config_ssm_prefix_with_slash_prefix}/backstage/db_credentials",
+ cluster_engine=aws_rds.DatabaseClusterEngine.aurora_postgres(
+ version=aws_rds.AuroraPostgresEngineVersion.VER_13_9
+ ),
+ rotation_interval_days=Duration.days(90),
+ )
+
+ cognito_construct = CognitoConstruct(
+ self,
+ "cognito-construct",
+ admin_user=module_inputs.admin_user,
+ email_invite_user_pool_name="Connected Mobility Solution - Backstage",
+ route53_construct=route53_construct,
+ )
+
+ backstage_container_construct = BackstageContainerConstruct(
+ self,
+ "backstage-container-construct",
+ module_inputs=module_inputs,
+ cognito_construct=cognito_construct,
+ postgres_database_construct=aurora_database_construct,
+ vpc=vpc_construct.vpc, # type: ignore[arg-type]
+ private_subnets=vpc_construct.private_subnet_selection,
+ route53_construct=route53_construct,
+ )
+
+ LoadBalancerConstruct(
+ self,
+ "load-balancer-construct",
+ backstage_container_construct=backstage_container_construct,
+ route53_construct=route53_construct,
+ cognito_construct=cognito_construct,
+ vpc=vpc_construct.vpc, # type: ignore[arg-type]
+ public_subnets=vpc_construct.public_subnet_selection,
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/aspects/__init__.py b/source/modules/acdp/backstage/cdk/source/infrastructure/aspects/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/aspects/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/backstage/cdk/source/infrastructure/aspects/backstage_nag_suppression.py b/source/modules/acdp/backstage/cdk/source/infrastructure/aspects/backstage_nag_suppression.py
similarity index 98%
rename from source/backstage/cdk/source/infrastructure/aspects/backstage_nag_suppression.py
rename to source/modules/acdp/backstage/cdk/source/infrastructure/aspects/backstage_nag_suppression.py
index 7c3c7cfd..17772843 100644
--- a/source/backstage/cdk/source/infrastructure/aspects/backstage_nag_suppression.py
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/aspects/backstage_nag_suppression.py
@@ -8,6 +8,8 @@
# Third Party Libraries
import jsii
+
+# AWS Libraries
from aws_cdk import CfnResource, IAspect
from constructs import IConstruct
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/buildspecs/backstage_deploy_buildspec.json b/source/modules/acdp/backstage/cdk/source/infrastructure/buildspecs/backstage_deploy_buildspec.json
new file mode 100644
index 00000000..4d128178
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/buildspecs/backstage_deploy_buildspec.json
@@ -0,0 +1,40 @@
+{
+ "version": "0.2",
+ "env": {
+ "variables": {}
+ },
+ "phases": {
+ "install": {
+ "runtime-versions": {
+ "nodejs": 18
+ },
+ "commands": [
+ "export PIPENV_VENV_IN_PROJECT=1",
+ "export PIPENV_IGNORE_VIRTUALENVS=1",
+ "cd cdk",
+ "export NODE_VERSION=$(cat .nvmrc)",
+ "n auto",
+ "npm install -g aws-cdk@latest --force",
+ "export PYTHON_VERSION=$(cat .python-version)",
+ "pyenv install ${PYTHON_VERSION}",
+ "pyenv global ${PYTHON_VERSION}",
+ "pyenv exec pip install pipenv",
+ "make install",
+ "cd .."
+ ]
+ },
+ "build": {
+ "commands": [
+ "export BACKSTAGE_IMAGE_TAG=s3_$CODEBUILD_RESOLVED_SOURCE_VERSION",
+ "cd cdk",
+ "chmod +x ./deployment/build-s3-dist.sh",
+ "chmod +x ./deployment/upload-s3-dist.sh",
+ "rm -f cdk.context.json",
+ "make build",
+ "make upload",
+ "make deploy",
+ "cd .."
+ ]
+ }
+ }
+}
diff --git a/source/backstage/cdk/source/infrastructure/buildspecs/backstage_image_buildspec.json b/source/modules/acdp/backstage/cdk/source/infrastructure/buildspecs/backstage_image_buildspec.json
similarity index 97%
rename from source/backstage/cdk/source/infrastructure/buildspecs/backstage_image_buildspec.json
rename to source/modules/acdp/backstage/cdk/source/infrastructure/buildspecs/backstage_image_buildspec.json
index f21a48b1..4dc46527 100644
--- a/source/backstage/cdk/source/infrastructure/buildspecs/backstage_image_buildspec.json
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/buildspecs/backstage_image_buildspec.json
@@ -9,7 +9,7 @@
},
"build": {
"commands": [
- "echo \"Building from $(pwd) [Docker Buildkit: $DOCKER_BUILDKIT - Node: $(node --version) - NPM: $(npm --version) - TSC: $(yarn tsc --version)]\"",
+ "echo \"Building from $(pwd) [Docker Buildkit: $DOCKER_BUILDKIT - Node: $(node --version) - NPM: $(npm --version)]\"",
"yarn build:backend",
"yarn build-image",
"docker tag backstage:latest $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_NAME:s3_$CODEBUILD_RESOLVED_SOURCE_VERSION",
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/__init__.py b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/aurora_database.py b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/aurora_database.py
new file mode 100644
index 00000000..d0aad62f
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/aurora_database.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# AWS Libraries
+from aws_cdk import Duration, aws_ec2, aws_rds
+from constructs import Construct
+
+
+class AuroraDatabaseConstruct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ vpc: aws_ec2.IVpc,
+ isolated_subnets: aws_ec2.SubnetSelection,
+ credentials_secret_name: str,
+ cluster_engine: aws_rds.IClusterEngine,
+ rotation_interval_days: Duration,
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ self.database_credentials_secret = aws_rds.DatabaseSecret(
+ self,
+ "database-secret",
+ username="db_admin",
+ secret_name=credentials_secret_name,
+ )
+
+ self.database_security_group = aws_ec2.SecurityGroup(
+ self, "database-security-group", vpc=vpc, allow_all_outbound=False
+ )
+
+ database = aws_rds.ServerlessCluster(
+ self,
+ "aurora-serverless-cluster",
+ engine=cluster_engine,
+ credentials=aws_rds.Credentials.from_secret(
+ self.database_credentials_secret
+ ),
+ vpc=vpc,
+ vpc_subnets=isolated_subnets,
+ deletion_protection=False, # deletion protection disabled to allow for graceful teardown
+ security_groups=[self.database_security_group],
+ )
+
+ database.add_rotation_single_user(
+ automatically_after=rotation_interval_days,
+ security_group=self.database_security_group,
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/backstage_container.py b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/backstage_container.py
new file mode 100644
index 00000000..2a88f172
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/backstage_container.py
@@ -0,0 +1,430 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import os
+
+# AWS Libraries
+from aws_cdk import (
+ ArnFormat,
+ Aws,
+ RemovalPolicy,
+ Stack,
+ aws_ec2,
+ aws_ecr,
+ aws_ecs,
+ aws_iam,
+ aws_kms,
+ aws_logs,
+ aws_secretsmanager,
+)
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..lib.cms_common.config.resource_names import ResourceName
+from .aurora_database import AuroraDatabaseConstruct
+from .cognito import CognitoConstruct
+from .module_integration import (
+ ModuleInputsConstruct,
+ SsmParameterWithAndWithoutSlashPrefix,
+)
+from .route53 import Route53Construct
+
+POSTGRES_PORT = 5432
+
+
+class BackstageContainerConstruct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ module_inputs: ModuleInputsConstruct,
+ cognito_construct: CognitoConstruct,
+ postgres_database_construct: AuroraDatabaseConstruct,
+ vpc: aws_ec2.IVpc,
+ private_subnets: aws_ec2.SubnetSelection,
+ route53_construct: Route53Construct,
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ backstage_task_definition_secrets = (
+ module_inputs.backstage_task_definition_secrets
+ )
+ backstage_configuration_properties = module_inputs.backstage_configuration
+ acdp_asset_config = module_inputs.acdp_asset_properties
+
+ ecs_cluster = aws_ecs.Cluster(
+ self,
+ "ecs-cluster",
+ vpc=vpc,
+ container_insights=True,
+ )
+
+ task_role = aws_iam.Role(
+ self,
+ "task-definition-role",
+ role_name=f"{module_inputs.acdp_uid}-{Stack.of(self).region}-backstage-task",
+ assumed_by=aws_iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
+ inline_policies={
+ "ssm-policy": aws_iam.PolicyDocument(
+ statements=[
+ aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=[
+ "ssm:GetParameter",
+ "ssm:PutParameter",
+ ],
+ resources=[
+ Stack.of(self).format_arn(
+ service="ssm",
+ resource="parameter",
+ resource_name=f"{module_inputs.acdp_build_ssm_prefix_without_slash_prefix}",
+ arn_format=ArnFormat.SLASH_RESOURCE_NAME,
+ ),
+ Stack.of(self).format_arn(
+ service="ssm",
+ resource="parameter",
+ resource_name=f"{module_inputs.acdp_build_ssm_prefix_without_slash_prefix}/*",
+ arn_format=ArnFormat.SLASH_RESOURCE_NAME,
+ ),
+ ],
+ ),
+ ],
+ ),
+ "s3-policy": aws_iam.PolicyDocument(
+ statements=[
+ aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=[
+ "s3:GetBucketAcl",
+ "s3:GetBucketLocation",
+ "s3:GetBucketVersioning",
+ "s3:GetObject",
+ "s3:GetObjectAcl",
+ "s3:GetObjectAttributes",
+ "s3:GetObjectVersion",
+ "s3:GetObjectVersionAcl",
+ "s3:GetObjectVersionTagging",
+ "s3:ListAllMyBuckets",
+ "s3:ListBucket",
+ "s3:ListBucketVersions",
+ ],
+ resources=[
+ Stack.of(self).format_arn(
+ service="s3",
+ resource=acdp_asset_config.regional_asset_bucket_name,
+ resource_name=None,
+ account="",
+ region="",
+ arn_format=ArnFormat.NO_RESOURCE_NAME,
+ ),
+ Stack.of(self).format_arn(
+ service="s3",
+ resource=acdp_asset_config.regional_asset_bucket_name,
+ resource_name="*",
+ account="",
+ region="",
+ arn_format=ArnFormat.SLASH_RESOURCE_NAME,
+ ),
+ ],
+ ),
+ aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=[
+ "s3:GetBucketAcl",
+ "s3:GetBucketLocation",
+ "s3:GetBucketVersioning",
+ "s3:GetObject",
+ "s3:GetObjectAcl",
+ "s3:GetObjectAttributes",
+ "s3:GetObjectVersion",
+ "s3:GetObjectVersionAcl",
+ "s3:GetObjectVersionTagging",
+ "s3:ListAllMyBuckets",
+ "s3:ListBucket",
+ "s3:ListBucketVersions",
+ "s3:PutObject",
+ "s3:DeleteObject",
+ "s3:DeleteObjectVersion",
+ ],
+ resources=[
+ Stack.of(self).format_arn(
+ service="s3",
+ resource=acdp_asset_config.local_asset_bucket_name,
+ resource_name=None,
+ account="",
+ region="",
+ arn_format=ArnFormat.NO_RESOURCE_NAME,
+ ),
+ Stack.of(self).format_arn(
+ service="s3",
+ resource=acdp_asset_config.local_asset_bucket_name,
+ resource_name=f"{acdp_asset_config.local_asset_bucket_root_key}/*",
+ account="",
+ region="",
+ arn_format=ArnFormat.SLASH_RESOURCE_NAME,
+ ),
+ Stack.of(self).format_arn(
+ service="s3",
+ resource=acdp_asset_config.local_asset_bucket_name,
+ resource_name=f"{acdp_asset_config.local_asset_bucket_default_assets_prefix}/*",
+ account="",
+ region="",
+ arn_format=ArnFormat.SLASH_RESOURCE_NAME,
+ ),
+ ],
+ ),
+ aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=[
+ "kms:GenerateDataKey",
+ "kms:Decrypt",
+ "kms:Encrypt",
+ ],
+ resources=[acdp_asset_config.local_asset_bucket_key_arn],
+ ),
+ ]
+ ),
+ "cognito-idp-policy": aws_iam.PolicyDocument(
+ statements=[
+ aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=[
+ "cognito-idp:DescribeUserPool",
+ "cognito-idp:DescribeUserPoolClient",
+ ],
+ resources=[
+ Stack.of(self).format_arn(
+ service="cognito-idp",
+ resource="userpool",
+ resource_name=cognito_construct.user_pool.user_pool_id,
+ arn_format=ArnFormat.SLASH_RESOURCE_NAME,
+ ),
+ ],
+ ),
+ ]
+ ),
+ "codebuild-policy": aws_iam.PolicyDocument(
+ statements=[
+ aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=[
+ "codebuild:StartBuild",
+ "codebuild:BatchGetProjects",
+ "codebuild:BatchGetBuilds",
+ "codebuild:ListBuildsForProject",
+ ],
+ resources=[
+ Stack.of(self).format_arn(
+ service="codebuild",
+ resource="project",
+ resource_name=f"{module_inputs.acdp_uid}-*",
+ arn_format=ArnFormat.SLASH_RESOURCE_NAME,
+ ),
+ ],
+ ),
+ ]
+ ),
+ },
+ )
+
+ task_definition = aws_ecs.FargateTaskDefinition(
+ self,
+ "fargate-task-definition",
+ cpu=1024,
+ memory_limit_mib=2048,
+ ephemeral_storage_gib=30,
+ family=Aws.STACK_NAME,
+ execution_role=task_role,
+ task_role=task_role,
+ )
+
+ container_log_group_kms_key = aws_kms.Key(
+ self,
+ "container-log-group-kms-key",
+ enable_key_rotation=True,
+ )
+
+ container_log_group = aws_logs.LogGroup(
+ self,
+ "container-log-group",
+ removal_policy=RemovalPolicy.RETAIN,
+ retention=aws_logs.RetentionDays.THREE_MONTHS,
+ encryption_key=container_log_group_kms_key,
+ )
+
+ container_log_group_kms_key.add_to_resource_policy(
+ statement=aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ principals=[
+ aws_iam.ServicePrincipal(
+ f"logs.{Stack.of(self).region}.amazonaws.com"
+ )
+ ],
+ actions=["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
+ resources=["*"],
+ )
+ )
+
+ task_definition.add_container(
+ f"{Aws.STACK_NAME}-container",
+ image=aws_ecs.ContainerImage.from_ecr_repository(
+ repository=aws_ecr.Repository.from_repository_name(
+ self,
+ "ecr-repository",
+ repository_name=backstage_configuration_properties.ecr_repository_name,
+ ),
+ tag=os.environ["BACKSTAGE_IMAGE_TAG"],
+ ),
+ port_mappings=[
+ aws_ecs.PortMapping(
+ container_port=8080,
+ protocol=aws_ecs.Protocol.TCP,
+ )
+ ],
+ container_name=f"{Aws.STACK_NAME}-backend",
+ secrets={ # Task definitions require ECS secrets, which can only be created from SecretsManager Secrets or SSM Parameters
+ "BACKSTAGE_NAME": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.backstage_name
+ ),
+ "BACKSTAGE_ORG": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.backstage_org
+ ),
+ "POSTGRES_USER": aws_ecs.Secret.from_secrets_manager(
+ postgres_database_construct.database_credentials_secret, "username"
+ ),
+ "POSTGRES_PASSWORD": aws_ecs.Secret.from_secrets_manager(
+ postgres_database_construct.database_credentials_secret, "password"
+ ),
+ "POSTGRES_HOST": aws_ecs.Secret.from_secrets_manager(
+ postgres_database_construct.database_credentials_secret, "host"
+ ),
+ "POSTGRES_PORT": aws_ecs.Secret.from_secrets_manager(
+ postgres_database_construct.database_credentials_secret, "port"
+ ),
+ "BACKEND_SECRET": aws_ecs.Secret.from_secrets_manager(
+ aws_secretsmanager.Secret(
+ self,
+ "backend-secret",
+ description="Backend secret",
+ secret_name=ResourceName.slash_separated(
+ prefix=module_inputs.acdp_config_ssm_prefix_with_slash_prefix,
+ name="backstage/backend-secret",
+ ),
+ generate_secret_string=aws_secretsmanager.SecretStringGenerator(),
+ )
+ ),
+ "REGIONAL_ASSET_BUCKET_NAME": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.regional_asset_bucket_name
+ ),
+ "REGIONAL_ASSET_BUCKET_REGION": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.regional_asset_bucket_region
+ ),
+ "REGIONAL_ASSET_BUCKET_BACKSTAGE_TEMPLATE_KEY_PREFIX": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.regional_asset_bucket_template_key_prefix
+ ),
+ "REGIONAL_ASSET_BUCKET_DISCOVERY_REFRESH_FREQ": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.regional_asset_bucket_discovery_refresh_frequency
+ ),
+ "REGIONAL_ASSET_BUCKET_BUILDSPEC_KEY_PREFIX": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.regional_asset_bucket_buildspec_key_prefix
+ ),
+ "LOCAL_ASSET_BUCKET_NAME": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.local_asset_bucket_name
+ ),
+ "LOCAL_ASSET_BUCKET_REGION": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.local_asset_bucket_region
+ ),
+ "LOCAL_ASSET_BUCKET_ROOT_KEY": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.local_asset_bucket_root_key_parameter
+ ),
+ "LOCAL_ASSET_BUCKET_BACKSTAGE_USER_PROVIDED_TEMPLATE_KEY_PREFIX": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.local_asset_bucket_backstage_user_provided_template_key_prefix
+ ),
+ "LOCAL_ASSET_BUCKET_BACKSTAGE_DEFAULT_TEMPLATE_KEY_PREFIX": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.local_asset_bucket_backstage_default_template_key_prefix
+ ),
+ "LOCAL_ASSET_BUCKET_CATALOG_KEY_PREFIX": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.local_asset_bucket_catalog_key_prefix
+ ),
+ "LOCAL_ASSET_BUCKET_TECHDOCS_KEY_PREFIX": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.local_asset_bucket_techdocs_key_prefix
+ ),
+ "LOCAL_ASSET_BUCKET_DISCOVERY_REFRESH_FREQ": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.local_asset_bucket_discovery_refresh_frequency_mins
+ ),
+ "CODEBUILD_PROJECT_ARN": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.codebuild_project_arn
+ ),
+ "ACDP_BUILD_CONFIG_SSM_PREFIX": aws_ecs.Secret.from_ssm_parameter(
+ backstage_task_definition_secrets.acdp_build_config_path_root_parameter
+ ),
+ "TARGET_ACCOUNT_ID": aws_ecs.Secret.from_ssm_parameter(
+ SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-backstage-target-account-id",
+ path_prefix_with_slash=module_inputs.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=module_inputs.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix="deployment-targets/default/account-id",
+ create_parameter=True,
+ create_parameter_value=Stack.of(self).account,
+ create_parameter_description="Backstage Deployment Target Account Id",
+ ).parameter_without_slash_prefix
+ ),
+ "TARGET_REGION": aws_ecs.Secret.from_ssm_parameter(
+ SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-backstage-target-region",
+ path_prefix_with_slash=module_inputs.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=module_inputs.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix="deployment-targets/default/region",
+ create_parameter=True,
+ create_parameter_value=Stack.of(self).region,
+ create_parameter_description="Backstage Deployment Target Region",
+ ).parameter_without_slash_prefix
+ ),
+ },
+ environment={
+ "SOLUTION_NAME": os.environ["SOLUTION_NAME"],
+ "SOLUTION_VERSION": os.environ["SOLUTION_VERSION"],
+ "WEB_HOSTNAME": route53_construct.base_domain,
+ "BACKEND_HOSTNAME": route53_construct.base_domain,
+ "NODE_ENV": backstage_configuration_properties.node_env,
+ "COGNITO_USERPOOL_ID": cognito_construct.user_pool.user_pool_id,
+ "LOG_LEVEL": backstage_configuration_properties.log_level,
+ "USER_AGENT_STRING": backstage_configuration_properties.user_agent_string,
+ },
+ logging=aws_ecs.LogDriver.aws_logs(
+ log_group=container_log_group,
+ stream_prefix=f"{Aws.STACK_NAME}-logs",
+ ),
+ )
+
+ self.fargate_security_group = aws_ec2.SecurityGroup(
+ self, "fargate-security-group", vpc=vpc, allow_all_outbound=True # NOSONAR
+ )
+
+ self.fargate_service = aws_ecs.FargateService(
+ self,
+ "fargate-service",
+ cluster=ecs_cluster,
+ task_definition=task_definition,
+ service_name=f"{Aws.STACK_NAME}-fargate-service",
+ desired_count=2,
+ assign_public_ip=False,
+ min_healthy_percent=50,
+ max_healthy_percent=200,
+ security_groups=[self.fargate_security_group],
+ vpc_subnets=private_subnets,
+ )
+
+ postgres_database_construct.database_security_group.add_ingress_rule(
+ peer=self.fargate_security_group,
+ connection=aws_ec2.Port.tcp(POSTGRES_PORT),
+ description="Allow ingress from fargate",
+ )
+
+ task_definition.default_container.add_environment( # type: ignore
+ "COGNITO_CLIENT_ID", cognito_construct.oidc_client.user_pool_client_id
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/cognito.py b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/cognito.py
new file mode 100644
index 00000000..ee8297c7
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/cognito.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from textwrap import dedent
+
+# AWS Libraries
+from aws_cdk import Duration, aws_cognito
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from .module_integration import AdminUserProperties
+from .route53 import Route53Construct
+
+
+class CognitoConstruct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ admin_user: AdminUserProperties,
+ email_invite_user_pool_name: str,
+ route53_construct: Route53Construct,
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ self.user_pool = aws_cognito.UserPool(
+ self,
+ "user-pool",
+ self_sign_up_enabled=False,
+ advanced_security_mode=aws_cognito.AdvancedSecurityMode.ENFORCED,
+ sign_in_aliases=aws_cognito.SignInAliases(
+ email=True,
+ username=True,
+ preferred_username=True,
+ ),
+ standard_attributes=aws_cognito.StandardAttributes(
+ email=aws_cognito.StandardAttribute(required=True, mutable=False),
+ fullname=aws_cognito.StandardAttribute(required=True, mutable=True),
+ preferred_username=aws_cognito.StandardAttribute(
+ required=False, mutable=True
+ ),
+ ),
+ account_recovery=aws_cognito.AccountRecovery.EMAIL_ONLY,
+ mfa=aws_cognito.Mfa.REQUIRED,
+ mfa_second_factor=aws_cognito.MfaSecondFactor(sms=False, otp=True),
+ user_verification=aws_cognito.UserVerificationConfig(
+ email_subject=f"{email_invite_user_pool_name} - Verify your email",
+ email_body="Thank you for signing up!\nClick here to verify your e-mail: {##Verify Email##}",
+ email_style=aws_cognito.VerificationEmailStyle.LINK,
+ sms_message=f"{email_invite_user_pool_name}\nYour verification code is {{####}}",
+ ),
+ user_invitation=aws_cognito.UserInvitationConfig(
+ email_subject=f"Invite to join {email_invite_user_pool_name}!",
+ email_body=dedent(
+ f"""\
+
+ Hello {{username}}, you have been invited to join {email_invite_user_pool_name}.
+ https://{route53_construct.base_domain}
+
+
+ Please sign in using the temporary credentials below:
+
+ Username: {{username}}
+ Password: {{####}}
+
+
+ """
+ ),
+ sms_message=f"Hello {{username}}, your temporary password for {email_invite_user_pool_name} is {{####}}",
+ ),
+ password_policy=aws_cognito.PasswordPolicy(
+ min_length=12,
+ require_lowercase=True,
+ require_uppercase=True,
+ require_digits=True,
+ require_symbols=True,
+ temp_password_validity=Duration.days(1),
+ ),
+ device_tracking=aws_cognito.DeviceTracking(
+ challenge_required_on_new_device=True,
+ device_only_remembered_on_user_prompt=True,
+ ),
+ )
+
+ self.oidc_client = self.user_pool.add_client(
+ "oidc-client",
+ generate_secret=True,
+ access_token_validity=Duration.hours(1),
+ auth_session_validity=Duration.minutes(3),
+ enable_token_revocation=True,
+ id_token_validity=Duration.hours(1),
+ prevent_user_existence_errors=True,
+ refresh_token_validity=Duration.hours(2),
+ o_auth=aws_cognito.OAuthSettings(
+ flows=aws_cognito.OAuthFlows(
+ authorization_code_grant=True,
+ ),
+ scopes=[aws_cognito.OAuthScope.OPENID],
+ callback_urls=[
+ f"https://{route53_construct.base_domain}/api/auth/cognito/handler/frame",
+ f"https://{route53_construct.base_domain}/oauth2/idpresponse",
+ ],
+ ),
+ )
+
+ aws_cognito.CfnUserPoolUser(
+ self,
+ "admin-user",
+ user_pool_id=self.user_pool.user_pool_id,
+ desired_delivery_mediums=["EMAIL"],
+ force_alias_creation=True,
+ user_attributes=[
+ {
+ "name": "email",
+ "value": admin_user.email,
+ },
+ {"name": "email_verified", "value": "true"},
+ ],
+ username=admin_user.username,
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/load_balancer.py b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/load_balancer.py
new file mode 100644
index 00000000..4c7964ea
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/load_balancer.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+
+# AWS Libraries
+from aws_cdk import (
+ Aws,
+ Stack,
+ aws_certificatemanager,
+ aws_cognito,
+ aws_ec2,
+ aws_elasticloadbalancingv2,
+ aws_route53,
+ aws_route53_targets,
+ aws_s3,
+)
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from .backstage_container import BackstageContainerConstruct
+from .cognito import CognitoConstruct
+from .route53 import Route53Construct
+
+FARGATE_PORT = 443
+
+
+class LoadBalancerConstruct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ route53_construct: Route53Construct,
+ backstage_container_construct: BackstageContainerConstruct,
+ cognito_construct: CognitoConstruct,
+ vpc: aws_ec2.IVpc,
+ public_subnets: aws_ec2.SubnetSelection,
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ alb_security_group = aws_ec2.SecurityGroup(
+ self, "alb-security-group", vpc=vpc, allow_all_outbound=True # NOSONAR
+ )
+
+ backstage_container_construct.fargate_security_group.add_ingress_rule(
+ alb_security_group,
+ connection=aws_ec2.Port.tcp(FARGATE_PORT),
+ description="alb security group to fargate security group connection",
+ )
+
+ alb = aws_elasticloadbalancingv2.ApplicationLoadBalancer(
+ self,
+ "application-load-balancer",
+ vpc=vpc,
+ vpc_subnets=public_subnets,
+ load_balancer_name=f"{Aws.STACK_NAME}-alb",
+ security_group=alb_security_group,
+ internet_facing=True,
+ drop_invalid_header_fields=True,
+ )
+
+ alb_access_logs_bucket = aws_s3.Bucket(
+ self,
+ "alb-access-logs-bucket",
+ block_public_access=aws_s3.BlockPublicAccess.BLOCK_ALL,
+ enforce_ssl=True,
+ versioned=True,
+ encryption=aws_s3.BucketEncryption.S3_MANAGED,
+ )
+
+ alb.log_access_logs(
+ bucket=alb_access_logs_bucket,
+ prefix="backstage-alb",
+ )
+
+ listener = alb.add_listener(
+ "listener",
+ port=FARGATE_PORT,
+ ssl_policy=aws_elasticloadbalancingv2.SslPolicy.TLS13_RES,
+ )
+
+ # ALB needs certificate in the same region as itself
+ listener_certificate = aws_certificatemanager.DnsValidatedCertificate(
+ self,
+ "listener-certificate",
+ hosted_zone=route53_construct.hosted_zone,
+ region=Stack.of(self).region,
+ domain_name=route53_construct.base_domain,
+ subject_alternative_names=[f"*.{route53_construct.base_domain}"],
+ )
+
+ listener.add_certificates(
+ "listener-certificates",
+ certificates=[
+ aws_elasticloadbalancingv2.ListenerCertificate.from_arn(
+ listener_certificate.certificate_arn
+ )
+ ],
+ )
+
+ target_group = listener.add_targets(
+ "fleet",
+ port=443,
+ protocol=aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,
+ targets=[backstage_container_construct.fargate_service],
+ )
+
+ aws_elasticloadbalancingv2.ApplicationListenerRule(
+ self,
+ "listener-rule",
+ priority=1,
+ listener=listener,
+ conditions=[
+ aws_elasticloadbalancingv2.ListenerCondition.path_patterns(["*"])
+ ],
+ target_groups=[target_group],
+ )
+
+ root_a_record = aws_route53.ARecord(
+ self,
+ "root-a-record",
+ zone=route53_construct.hosted_zone,
+ record_name=f"{route53_construct.base_domain}.",
+ target=aws_route53.RecordTarget.from_alias(
+ aws_route53_targets.LoadBalancerTarget(alb)
+ ),
+ )
+
+ # Cognito only supports certificates in us-east-1
+ cognito_certificate = aws_certificatemanager.DnsValidatedCertificate(
+ self,
+ "user-pool-domain-certificate",
+ hosted_zone=route53_construct.hosted_zone,
+ region="us-east-1",
+ domain_name=route53_construct.base_domain,
+ subject_alternative_names=[f"*.{route53_construct.base_domain}"],
+ )
+
+ self.user_pool_domain = cognito_construct.user_pool.add_domain(
+ "user-pool-domain",
+ custom_domain=aws_cognito.CustomDomainOptions(
+ certificate=aws_elasticloadbalancingv2.ListenerCertificate.from_arn( # type: ignore[arg-type]
+ cognito_certificate.certificate_arn
+ ),
+ domain_name=f"auth.{route53_construct.base_domain}",
+ ),
+ )
+ self.user_pool_domain.node.add_dependency(root_a_record)
+
+ aws_route53.ARecord(
+ self,
+ "cognito-a-record",
+ zone=route53_construct.hosted_zone,
+ record_name=f"auth.{route53_construct.base_domain}.",
+ target=aws_route53.RecordTarget.from_alias(
+ aws_route53_targets.UserPoolDomainTarget(self.user_pool_domain)
+ ),
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/module_integration.py b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/module_integration.py
new file mode 100644
index 00000000..d5cfcc62
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/module_integration.py
@@ -0,0 +1,406 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from dataclasses import dataclass
+from typing import Optional
+
+# AWS Libraries
+from aws_cdk import CfnParameter, Stack, aws_ssm
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..lib.cms_common.config.resource_names import ResourceName, ResourcePrefix
+from ..lib.cms_common.config.stack_inputs import SolutionConfigInputs
+from ..lib.cms_common.constructs.vpc_construct import create_vpc_config
+
+
+@dataclass(frozen=True)
+class AdminUserProperties:
+ email: str
+ username: str
+
+
+@dataclass(frozen=True)
+class BackstageConfigurationProperties:
+ ecr_repository_name: str
+ node_env: str
+ log_level: str
+ user_agent_string: str
+
+
+@dataclass(frozen=True)
+class AcdpAssetProperties:
+ regional_asset_bucket_name: str
+ local_asset_bucket_name: str
+ local_asset_bucket_key_arn: str
+ local_asset_bucket_root_key: str
+ local_asset_bucket_default_assets_prefix: str
+
+
+@dataclass(frozen=True)
+class BackstageTaskDefinitionSecrets:
+ backstage_name: aws_ssm.IStringParameter
+ backstage_org: aws_ssm.IStringParameter
+ regional_asset_bucket_name: aws_ssm.IStringParameter
+ regional_asset_bucket_region: aws_ssm.IStringParameter
+ regional_asset_bucket_template_key_prefix: aws_ssm.IStringParameter
+ regional_asset_bucket_buildspec_key_prefix: aws_ssm.IStringParameter
+ regional_asset_bucket_discovery_refresh_frequency: aws_ssm.IStringParameter
+ local_asset_bucket_name: aws_ssm.IStringParameter
+ local_asset_bucket_region: aws_ssm.IStringParameter
+ local_asset_bucket_kms_key_arn: aws_ssm.IStringParameter
+ local_asset_bucket_root_key_parameter: aws_ssm.IStringParameter
+ local_asset_bucket_default_assets_prefix_parameter: aws_ssm.IStringParameter
+ local_asset_bucket_backstage_user_provided_template_key_prefix: aws_ssm.IStringParameter
+ local_asset_bucket_backstage_default_template_key_prefix: aws_ssm.IStringParameter
+ local_asset_bucket_catalog_key_prefix: aws_ssm.IStringParameter
+ local_asset_bucket_techdocs_key_prefix: aws_ssm.IStringParameter
+ local_asset_bucket_discovery_refresh_frequency_mins: aws_ssm.IStringParameter
+ codebuild_project_arn: aws_ssm.IStringParameter
+ acdp_build_config_path_root_parameter: aws_ssm.IStringParameter
+
+
+class SsmParameterWithAndWithoutSlashPrefix(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ path_prefix_with_slash: str,
+ path_prefix_without_slash: str,
+ path_postfix: str,
+ create_parameter: bool = False,
+ create_parameter_value: Optional[str] = None,
+ create_parameter_description: Optional[str] = None,
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ if create_parameter:
+ if create_parameter_value is None:
+ raise ValueError(
+ "create_parameter_value must be set when creating a parameter"
+ )
+ new_parameter = aws_ssm.StringParameter(
+ self,
+ "create-ssm-param",
+ string_value=create_parameter_value,
+ description=create_parameter_description,
+ parameter_name=ResourceName.slash_separated(
+ prefix=path_prefix_with_slash,
+ name=path_postfix,
+ ),
+ simple_name=True,
+ )
+ self.string_value = new_parameter.string_value
+ else:
+ parameter_with_slash_prefix = (
+ aws_ssm.StringParameter.from_string_parameter_attributes(
+ self,
+ "ssm-param-with-slash-prefix",
+ parameter_name=ResourceName.slash_separated(
+ prefix=path_prefix_with_slash,
+ name=path_postfix,
+ ),
+ simple_name=True,
+ force_dynamic_reference=True,
+ )
+ )
+
+ self.string_value = parameter_with_slash_prefix.string_value
+
+ self.parameter_without_slash_prefix = (
+ aws_ssm.StringParameter.from_string_parameter_attributes(
+ self,
+ "ssm-param-without-slash-prefix",
+ parameter_name=ResourceName.slash_separated(
+ prefix=path_prefix_without_slash,
+ name=path_postfix,
+ ),
+ simple_name=True,
+ force_dynamic_reference=True,
+ )
+ )
+
+
+class ModuleInputsConstruct(Construct):
+ # pylint: disable=R0914
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ solution_config_inputs: SolutionConfigInputs,
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ self.acdp_uid = CfnParameter(
+ Stack.of(self),
+ "AcdpUniqueId",
+ description="Name of the ACDP deployment",
+ default="acdp",
+ type="String",
+ ).value_as_string
+
+ self.vpc_name = CfnParameter(
+ Stack.of(self),
+ "VpcName",
+ description="name of the imported vpc",
+ type="String",
+ ).value_as_string
+
+ self.vpc_config = create_vpc_config(
+ vpc_name=self.vpc_name,
+ )
+
+ self.acdp_config_ssm_prefix_with_slash_prefix = ResourcePrefix.slash_separated(
+ app_unique_id=self.acdp_uid, module_name="config", leading_slash=True
+ )
+
+ self.acdp_config_ssm_prefix_without_slash_prefix = (
+ ResourcePrefix.slash_separated(
+ app_unique_id=self.acdp_uid, module_name="config", leading_slash=False
+ )
+ )
+
+ self.acdp_build_ssm_prefix_with_slash_prefix = ResourcePrefix.slash_separated(
+ app_unique_id=self.acdp_uid, module_name="acdp-build", leading_slash=True
+ )
+
+ self.acdp_build_ssm_prefix_without_slash_prefix = (
+ ResourcePrefix.slash_separated(
+ app_unique_id=self.acdp_uid,
+ module_name="acdp-build",
+ leading_slash=False,
+ )
+ )
+
+ regional_asset_config_ssm_path = "acdp-asset-config/regional"
+ local_asset_config_ssm_path = "acdp-asset-config/local"
+
+ regional_asset_bucket_name_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-regional-asset-bucket-name-parameter",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{regional_asset_config_ssm_path}/asset-bucket/name",
+ )
+
+ regional_asset_bucket_region_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-regional-asset-bucket-region-parameter",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{regional_asset_config_ssm_path}/asset-bucket/region",
+ )
+
+ regional_asset_bucket_template_key_prefix_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-regional-asset-bucket-template-key-prefix-parameter",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{regional_asset_config_ssm_path}/backstage-template-key-prefix",
+ )
+
+ regional_asset_bucket_buildspec_key_prefix_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-regional-asset-bucket-buildspec-key-prefix-parameter",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{regional_asset_config_ssm_path}/buildspec-key-prefix",
+ )
+
+ regional_asset_bucket_discovery_refresh_frequency_mins_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-regional-asset-bucket-refresh-frequency-mins-parameter",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{regional_asset_config_ssm_path}/discovery-refresh-frequency-mins",
+ )
+
+ local_asset_bucket_name_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-local-asset-bucket-name-parameter",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{local_asset_config_ssm_path}/asset-bucket/name",
+ )
+
+ local_asset_bucket_kms_key_arn_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-local-asset-config-bucket-key-arn",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{local_asset_config_ssm_path}/asset-bucket/key-arn",
+ )
+
+ local_asset_bucket_region_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-local-asset-bucket-region-parameter",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{local_asset_config_ssm_path}/asset-bucket/region",
+ )
+
+ local_asset_bucket_root_key_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-local-asset-config-asset-bucket-root-key",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{local_asset_config_ssm_path}/root-s3-key",
+ )
+
+ local_asset_bucket_default_assets_prefix_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-local-asset-config-asset-default-assets-prefix",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{local_asset_config_ssm_path}/default-assets-prefix",
+ )
+
+ local_asset_bucket_custom_template_key_prefix_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-local-asset-config-asset-bucket-backstage-custom-template-key-prefix",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{local_asset_config_ssm_path}/backstage-custom-template-key-prefix",
+ )
+
+ local_asset_bucket_default_template_key_prefix_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-local-asset-config-asset-bucket-backstage-default-template-key-prefix",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{local_asset_config_ssm_path}/backstage-default-template-key-prefix",
+ )
+
+ local_asset_bucket_catalog_key_prefix_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-local-asset-config-asset-bucket-catalog-key-prefix",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{local_asset_config_ssm_path}/catalog-key-prefix",
+ )
+
+ local_asset_bucket_techdocs_key_prefix_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-local-asset-config-asset-bucket-techdocs-key-prefix",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{local_asset_config_ssm_path}/techdocs-key-prefix",
+ )
+
+ local_asset_bucket_discovery_refresh_frequency_mins_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-local-asset-config-backstage-discovery-refresh-freq-mins",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix=f"{local_asset_config_ssm_path}/discovery-refresh-frequency-mins",
+ )
+
+ codebuild_project_arn_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-codebuild-project-arn-parameter",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix="codebuild-project/arn",
+ )
+
+ backstage_name_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-backstage-name-parameter",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix="backstage/name",
+ )
+
+ backstage_org_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-backstage-org-parameter",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix="backstage/organization",
+ )
+
+ backstage_ecr_repository_name_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-backstage-ecr-repository-name",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix="backstage/ecr-repository/name",
+ )
+
+ backstage_log_level_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-backstage-log-level",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix="backstage/log-level",
+ )
+
+ backstage_admin_email_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-backstage-admin-email",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix="backstage/admin-email",
+ )
+
+ acdp_build_config_path_root_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-acdp-build-ssm-prefix",
+ path_prefix_with_slash=self.acdp_build_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_build_ssm_prefix_without_slash_prefix,
+ path_postfix="build-parameters",
+ create_parameter=True,
+ create_parameter_value=self.acdp_build_ssm_prefix_with_slash_prefix,
+ )
+
+ backstage_admin_username_parameter = SsmParameterWithAndWithoutSlashPrefix(
+ self,
+ "ssm-backstage-admin-username",
+ path_prefix_with_slash=self.acdp_config_ssm_prefix_with_slash_prefix,
+ path_prefix_without_slash=self.acdp_config_ssm_prefix_without_slash_prefix,
+ path_postfix="backstage/admin-username",
+ )
+
+ self.acdp_asset_properties = AcdpAssetProperties(
+ regional_asset_bucket_name=regional_asset_bucket_name_parameter.string_value,
+ local_asset_bucket_name=local_asset_bucket_name_parameter.string_value,
+ local_asset_bucket_key_arn=local_asset_bucket_kms_key_arn_parameter.string_value,
+ local_asset_bucket_root_key=local_asset_bucket_root_key_parameter.string_value,
+ local_asset_bucket_default_assets_prefix=local_asset_bucket_default_assets_prefix_parameter.string_value,
+ )
+
+ self.backstage_configuration = BackstageConfigurationProperties(
+ ecr_repository_name=backstage_ecr_repository_name_parameter.string_value,
+ node_env="production",
+ log_level=backstage_log_level_parameter.string_value,
+ user_agent_string=solution_config_inputs.get_user_agent_string(),
+ )
+
+ self.admin_user = AdminUserProperties(
+ email=backstage_admin_email_parameter.string_value,
+ username=backstage_admin_username_parameter.string_value,
+ )
+
+ self.backstage_task_definition_secrets = BackstageTaskDefinitionSecrets(
+ backstage_name=backstage_name_parameter.parameter_without_slash_prefix,
+ backstage_org=backstage_org_parameter.parameter_without_slash_prefix,
+ regional_asset_bucket_name=regional_asset_bucket_name_parameter.parameter_without_slash_prefix,
+ regional_asset_bucket_region=regional_asset_bucket_region_parameter.parameter_without_slash_prefix,
+ regional_asset_bucket_template_key_prefix=regional_asset_bucket_template_key_prefix_parameter.parameter_without_slash_prefix,
+ regional_asset_bucket_buildspec_key_prefix=regional_asset_bucket_buildspec_key_prefix_parameter.parameter_without_slash_prefix,
+ regional_asset_bucket_discovery_refresh_frequency=regional_asset_bucket_discovery_refresh_frequency_mins_parameter.parameter_without_slash_prefix,
+ local_asset_bucket_name=local_asset_bucket_name_parameter.parameter_without_slash_prefix,
+ local_asset_bucket_region=local_asset_bucket_region_parameter.parameter_without_slash_prefix,
+ local_asset_bucket_kms_key_arn=local_asset_bucket_kms_key_arn_parameter.parameter_without_slash_prefix,
+ local_asset_bucket_root_key_parameter=local_asset_bucket_root_key_parameter.parameter_without_slash_prefix,
+ local_asset_bucket_default_assets_prefix_parameter=local_asset_bucket_default_assets_prefix_parameter.parameter_without_slash_prefix,
+ local_asset_bucket_backstage_user_provided_template_key_prefix=local_asset_bucket_custom_template_key_prefix_parameter.parameter_without_slash_prefix,
+ local_asset_bucket_backstage_default_template_key_prefix=local_asset_bucket_default_template_key_prefix_parameter.parameter_without_slash_prefix,
+ local_asset_bucket_catalog_key_prefix=local_asset_bucket_catalog_key_prefix_parameter.parameter_without_slash_prefix,
+ local_asset_bucket_techdocs_key_prefix=local_asset_bucket_techdocs_key_prefix_parameter.parameter_without_slash_prefix,
+ local_asset_bucket_discovery_refresh_frequency_mins=local_asset_bucket_discovery_refresh_frequency_mins_parameter.parameter_without_slash_prefix,
+ codebuild_project_arn=codebuild_project_arn_parameter.parameter_without_slash_prefix,
+ acdp_build_config_path_root_parameter=acdp_build_config_path_root_parameter.parameter_without_slash_prefix,
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/route53.py b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/route53.py
new file mode 100644
index 00000000..8ecdd1bb
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/constructs/route53.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+
+# AWS Libraries
+from aws_cdk import aws_route53
+from constructs import Construct
+
+
+class Route53Construct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ route53_hosted_zone_name: str,
+ route53_base_domain: str,
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ self.zone_name = route53_hosted_zone_name
+ self.base_domain = route53_base_domain
+
+ self.hosted_zone = aws_route53.HostedZone.from_lookup( # NOTE: MAKE SURE TO EXPORT PROPER AWS_ACCOUNT_ID
+ self,
+ "hosted-zone",
+ domain_name=route53_hosted_zone_name,
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/__init__.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/__init__.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/aspects/__init__.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/aspects/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/aspects/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/aspects/vpc_aspect.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/aspects/vpc_aspect.py
new file mode 100644
index 00000000..0d87ba00
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/aspects/vpc_aspect.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import re
+from typing import Any, Dict, List
+
+# Third Party Libraries
+import jsii
+
+# AWS Libraries
+from aws_cdk import CfnResource, IAspect
+from constructs import IConstruct
+
+
+def make_vpc_cfn_config(
+ security_group_logical_ids: List[str], subnet_names: List[str]
+) -> Dict[str, Any]:
+ return {
+ "SecurityGroupIds": [
+ {
+ "Fn::GetAtt": [
+ security_group_logical_id,
+ "GroupId",
+ ]
+ }
+ for security_group_logical_id in security_group_logical_ids
+ ],
+ "SubnetIds": subnet_names,
+ }
+
+
+def generate_ec2_vpc_policy_cfn_format(subnet_names: List[str]) -> Dict[str, Any]:
+ return {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "ec2:CreateNetworkInterfacePermission",
+ ],
+ "Condition": {
+ "StringEquals": {
+ "ec2:Subnet": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {"Ref": "AWS::Partition"},
+ ":ec2:",
+ {"Ref": "AWS::Region"},
+ ":",
+ {"Ref": "AWS::AccountId"},
+ ":subnet/",
+ subnet_name,
+ ],
+ ]
+ }
+ for subnet_name in subnet_names
+ ],
+ "ec2:AuthorizedService": "lambda.amazonaws.com",
+ }
+ },
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {"Ref": "AWS::Partition"},
+ ":ec2:",
+ {"Ref": "AWS::Region"},
+ ":",
+ {"Ref": "AWS::AccountId"},
+ ":network-interface/*",
+ ],
+ ]
+ },
+ },
+ {
+ "Action": [
+ "ec2:DescribeNetworkInterfaces",
+ "ec2:CreateNetworkInterface",
+ "ec2:DeleteNetworkInterface",
+ ],
+ "Effect": "Allow",
+ "Resource": "*",
+ },
+ ],
+ "Version": "2012-10-17",
+ },
+ "PolicyName": "ec2-policy",
+ }
+
+
+@jsii.implements(IAspect)
+class ApplyVpcOnCustomResource:
+ def __init__(
+ self,
+ module_name: str,
+ security_group_logical_ids: List[str],
+ subnet_names: List[str],
+ ) -> None:
+ self.vpc_config = make_vpc_cfn_config(
+ security_group_logical_ids=security_group_logical_ids,
+ subnet_names=subnet_names,
+ )
+
+ self.ec2_cfn_policy = generate_ec2_vpc_policy_cfn_format(
+ subnet_names=subnet_names
+ )
+
+ self.service_resource_patterns = [
+ rf"^/{module_name}/LogRetention[a-zA-Z0-9]+/Resource$",
+ rf"^/{module_name}/AWS[a-zA-Z0-9]+/Resource$",
+ ]
+
+ self.service_role_patterns = [
+ rf"^/{module_name}/LogRetention[a-zA-Z0-9]+/ServiceRole/Resource$",
+ rf"^/{module_name}/AWS[a-zA-Z0-9]+/ServiceRole/Resource$",
+ ]
+
+ def visit(
+ self,
+ node: IConstruct,
+ ) -> None:
+ node_path = f"/{node.node.path}"
+ vpc_config_property_path = "VpcConfig"
+ policy_path = "Policies"
+
+ if any(
+ re.match(pattern, node_path) is not None
+ for pattern in self.service_resource_patterns
+ ):
+ CfnResource.add_property_override(
+ node, vpc_config_property_path, self.vpc_config # type: ignore[arg-type]
+ )
+ elif any(
+ re.match(pattern, node_path) is not None
+ for pattern in self.service_role_patterns
+ ):
+ CfnResource.add_property_override(
+ node, # type: ignore[arg-type]
+ policy_path,
+ [self.ec2_cfn_policy],
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/__init__.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/resource_names.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/resource_names.py
new file mode 100644
index 00000000..a5922ab6
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/resource_names.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+# AWS Libraries
+from aws_cdk import Fn
+
+SOLUTIONS_PREFIX = "solution"
+
+# NOTE: These functions must use Cfn functions for string manipulation since AppUniqueId can be a token and therefore not resolvable yet in the template.
+# This is not necessary for basic string concatenation or f strings as Cloud Formation handles this automatically.
+
+
+def get_application_level_path_prefix(
+ app_unique_id: str, leading_slash: bool = False
+) -> str:
+ path_prefix = f"{SOLUTIONS_PREFIX}/{app_unique_id}"
+ return f"/{path_prefix}" if leading_slash else path_prefix
+
+
+class ResourcePrefix:
+ @staticmethod
+ def slash_separated(
+ app_unique_id: str, module_name: str, leading_slash: bool = False
+ ) -> str:
+ return f"{get_application_level_path_prefix(app_unique_id=app_unique_id, leading_slash=leading_slash)}/{module_name}"
+
+ @staticmethod
+ def hyphen_separated(app_unique_id: str, module_name: str) -> str:
+ return f"{app_unique_id}-{module_name}"
+
+ @staticmethod
+ def only_underscore_separated(app_unique_id: str, module_name: str) -> str:
+ prefix = f"{app_unique_id}_{module_name}"
+ return Fn.join("_", Fn.split("-", prefix))
+
+
+class ResourceName:
+ @staticmethod
+ def slash_separated(prefix: str, name: str) -> str:
+ return f"{prefix}/{name}"
+
+ @staticmethod
+ def hyphen_separated(prefix: str, name: str) -> str:
+ return f"{prefix}-{name}"
+
+ @staticmethod
+ def underscore_separated(prefix: str, name: str) -> str:
+ return f"{prefix}_{name}"
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/ssm.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/ssm.py
new file mode 100644
index 00000000..9b3ca83d
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/ssm.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+
+def resolve_ssm_parameter(parameter_name: str) -> str:
+ # parameter_name should have any leading slashes expected in the ssm parameter name
+ return f"{{{{resolve:ssm:{parameter_name}}}}}"
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/stack_inputs.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/stack_inputs.py
new file mode 100644
index 00000000..910b4b28
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/config/stack_inputs.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from dataclasses import dataclass
+from typing import Optional
+
+# AWS Libraries
+from aws_cdk import App, Tags
+
+
+@dataclass(frozen=True)
+class S3AssetConfigInputs:
+ bucket_base_name: str
+ object_key_prefix: str
+
+
+@dataclass(frozen=True)
+class SolutionConfigInputs:
+ solution_name: str
+ solution_id: str
+ solution_version: str
+ application_type: str
+ module_name: str
+ module_short_name: str
+ capability_id: Optional[str]
+
+ def get_user_agent_string(self) -> str:
+ if self.capability_id is None:
+ return f"AWSSOLUTION/{self.solution_id}/{self.solution_version}"
+
+ return f"AWSSOLUTION/{self.solution_id}/{self.solution_version} AWSSOLUTION-CAPABILITY/{self.capability_id}/{self.solution_version}"
+
+
+def create_stack_description(solution_config: SolutionConfigInputs) -> str:
+ return (
+ f"({solution_config.solution_id}-{solution_config.capability_id}) "
+ f"{solution_config.solution_name} - {solution_config.module_name}. "
+ f"Version {solution_config.solution_version}"
+ )
+
+
+def create_solution_tags_for_stack(
+ app: App, solution_config: SolutionConfigInputs
+) -> None:
+ Tags.of(app).add("Solutions:ModuleName", solution_config.module_name)
+ Tags.of(app).add("Solutions:SolutionName", solution_config.solution_name)
+ Tags.of(app).add("Solutions:SolutionID", solution_config.solution_id)
+ Tags.of(app).add("Solutions:SolutionVersion", solution_config.solution_version)
+ Tags.of(app).add("Solutions:ApplicationType", solution_config.application_type)
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/__init__.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/app_unique_id.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/app_unique_id.py
new file mode 100644
index 00000000..e57ac947
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/app_unique_id.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# AWS Libraries
+from aws_cdk import CfnParameter, aws_ssm
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..config.resource_names import ResourcePrefix, get_application_level_path_prefix
+from ..config.ssm import resolve_ssm_parameter
+
+
+class AppUniqueId:
+ @staticmethod
+ def create_cfn_parameter(
+ scope: Construct,
+ ) -> str:
+ app_unique_id = CfnParameter(
+ scope,
+ "AppUniqueId",
+ type="String",
+ description="Application unique identifier used to uniquely name resources within the stack.",
+ allowed_pattern=r"^(?!-)[a-z0-9-]+(? aws_ssm.StringParameter:
+ return aws_ssm.StringParameter(
+ scope,
+ "ssm-app-unique-id",
+ parameter_name=f"/{get_application_level_path_prefix(app_unique_id)}",
+ string_value=app_unique_id,
+ description="SSM parameter to register an app unique ID.",
+ simple_name=True,
+ )
+
+ @staticmethod
+ def register_module(
+ scope: Construct, app_unique_id: str, module_name: str
+ ) -> aws_ssm.StringParameter:
+ return aws_ssm.StringParameter(
+ scope,
+ "ssm-app-unique-id-register-module",
+ parameter_name=ResourcePrefix.slash_separated(
+ app_unique_id=app_unique_id,
+ module_name=module_name,
+ leading_slash=True,
+ ),
+ string_value=resolve_ssm_parameter(
+ parameter_name=get_application_level_path_prefix(
+ app_unique_id, leading_slash=True
+ )
+ ),
+ description="SSM parameter to register a module with an app unique ID.",
+ simple_name=True,
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/cdk_lambda_vpc_config_construct.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/cdk_lambda_vpc_config_construct.py
new file mode 100644
index 00000000..e17dd739
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/cdk_lambda_vpc_config_construct.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from typing import List
+
+# AWS Libraries
+from aws_cdk import Stack, aws_ec2
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..policy_generators.ec2_vpc import generate_ec2_vpc_policy
+from .vpc_construct import VpcConstruct
+
+
+class CDKLambdasVpcConfigConstruct(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ vpc_construct: VpcConstruct,
+ subnets: List[str],
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ base_security_group = aws_ec2.SecurityGroup(
+ self, "security-group", allow_all_outbound=True, vpc=vpc_construct.vpc # type: ignore[arg-type] # NOSONAR
+ )
+
+ self.security_groups = [
+ Stack.of(self).get_logical_id(base_security_group.node.default_child) # type: ignore[arg-type]
+ ]
+
+ self.subnets = subnets
+ self.ec2_vpc_policy_document = generate_ec2_vpc_policy(
+ self,
+ vpc_construct=vpc_construct,
+ subnet_selection=vpc_construct.private_subnet_selection,
+ authorized_service="lambda.amazonaws.com",
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/vpc_construct.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/vpc_construct.py
new file mode 100644
index 00000000..0e7d7775
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/constructs/vpc_construct.py
@@ -0,0 +1,241 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from typing import Any, Dict, List
+
+# Third Party Libraries
+import jsii
+from attrs import define
+
+# AWS Libraries
+from aws_cdk import Stack, aws_ec2, aws_iam
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..config.resource_names import SOLUTIONS_PREFIX, ResourceName
+from ..config.ssm import resolve_ssm_parameter
+
+
+@define(auto_attribs=True, frozen=True)
+class VpcConfig:
+ vpc_name: str
+ vpc_id: str
+ public_subnets: List[str]
+ private_subnets: List[str]
+ isolated_subnets: List[str]
+ availability_zones: List[str]
+
+
+def create_vpc_config(vpc_name: str) -> VpcConfig:
+ vpc_ssm_prefix = f"/{SOLUTIONS_PREFIX}/vpc/{vpc_name}"
+ return VpcConfig(
+ vpc_name=vpc_name,
+ vpc_id=resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="vpcid"
+ )
+ ),
+ public_subnets=[
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/public/1"
+ )
+ ),
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/public/2"
+ )
+ ),
+ ],
+ private_subnets=[
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/private/1"
+ )
+ ),
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/private/2"
+ )
+ ),
+ ],
+ isolated_subnets=[
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/isolated/1"
+ )
+ ),
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="subnets/isolated/2"
+ )
+ ),
+ ],
+ availability_zones=[
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="azs/1"
+ )
+ ),
+ resolve_ssm_parameter(
+ parameter_name=ResourceName.slash_separated(
+ prefix=vpc_ssm_prefix, name="azs/2"
+ )
+ ),
+ ],
+ )
+
+
+class IncorrectSubnetType(Exception):
+ ...
+
+
+@jsii.implements(aws_ec2.IVpc)
+class UnsafeDynamicVpc(Construct):
+ def __init__(
+ self,
+ scope: Construct,
+ construct_id: str,
+ vpc_id: str,
+ vpc_name: str,
+ public_subnets: List[aws_ec2.ISubnet],
+ private_subnets: List[aws_ec2.ISubnet],
+ isolated_subnets: List[aws_ec2.ISubnet],
+ availability_zones: List[str],
+ ) -> None:
+ super().__init__(scope, construct_id)
+ self._vpc_id = vpc_id
+ self._vpc_name = vpc_name
+ self._vpc_arn = Stack.of(self).format_arn(
+ service="ec2", resource="vpc", resource_name=self.vpc_id
+ )
+
+ self._public_subnets = public_subnets
+ self._private_subnets = private_subnets
+ self._isolated_subnets = isolated_subnets
+ self._availability_zones = availability_zones
+
+ @property
+ def vpc_id(self) -> str:
+ return self._vpc_id
+
+ @property
+ def availability_zones(self) -> List[str]:
+ return self._availability_zones
+
+ @property
+ def public_subnets(self) -> List[aws_ec2.ISubnet]:
+ return self._public_subnets
+
+ @property
+ def private_subnets(self) -> List[aws_ec2.ISubnet]:
+ return self._private_subnets
+
+ @property
+ def isolated_subnets(self) -> List[aws_ec2.ISubnet]:
+ return self._isolated_subnets
+
+ @property
+ def vpc_arn(self) -> str:
+ return self._vpc_arn
+
+ def select_subnets(self, selection: aws_ec2.SubnetSelection) -> Dict[str, Any]:
+ ### As of now this function only supports selection of subnet by types
+ selected_subnets = None
+
+ has_public = False
+ match (selection.subnet_type):
+ case aws_ec2.SubnetType.PUBLIC:
+ selected_subnets = self._public_subnets
+ has_public = True
+ case aws_ec2.SubnetType.PRIVATE_WITH_EGRESS:
+ selected_subnets = self._private_subnets
+ case aws_ec2.SubnetType.PRIVATE_ISOLATED:
+ selected_subnets = self._isolated_subnets
+
+ if not selected_subnets:
+ raise IncorrectSubnetType
+
+ internet_connectivity_established = aws_iam.CompositeDependable(
+ *[subnet.internet_connectivity_established for subnet in selected_subnets]
+ )
+ return {
+ "subnetIds": [subnet.subnet_id for subnet in selected_subnets],
+ "availabilityZones": self._availability_zones,
+ "hasPublic": has_public,
+ "subnets": selected_subnets,
+ "internetConnectivityEstablished": internet_connectivity_established,
+ }
+
+
+class VpcConstruct(Construct):
+ def __init__(
+ self, scope: Construct, construct_id: str, vpc_config: VpcConfig
+ ) -> None:
+ super().__init__(scope, construct_id)
+
+ self.public_subnets = [
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "public-subnet-1",
+ subnet_id=vpc_config.public_subnets[0],
+ ),
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "public-subnet-2",
+ subnet_id=vpc_config.public_subnets[1],
+ ),
+ ]
+
+ self.public_subnet_selection = aws_ec2.SubnetSelection(
+ subnets=self.public_subnets, subnet_type=aws_ec2.SubnetType.PUBLIC
+ )
+
+ self.private_subnets = [
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "private-subnet-1",
+ subnet_id=vpc_config.private_subnets[0],
+ ),
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "private-subnet-2",
+ subnet_id=vpc_config.private_subnets[1],
+ ),
+ ]
+
+ self.private_subnet_selection = aws_ec2.SubnetSelection(
+ subnets=self.private_subnets,
+ subnet_type=aws_ec2.SubnetType.PRIVATE_WITH_EGRESS,
+ )
+
+ self.isolated_subnets = [
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "isolated-subnet-1",
+ subnet_id=vpc_config.isolated_subnets[0],
+ ),
+ aws_ec2.Subnet.from_subnet_attributes(
+ self,
+ "isolated-subnet-2",
+ subnet_id=vpc_config.isolated_subnets[1],
+ ),
+ ]
+
+ self.isolated_subnet_selection = aws_ec2.SubnetSelection(
+ subnets=self.isolated_subnets,
+ subnet_type=aws_ec2.SubnetType.PRIVATE_ISOLATED,
+ )
+
+ self.vpc = UnsafeDynamicVpc(
+ self,
+ "acdp-vpc",
+ vpc_id=vpc_config.vpc_id,
+ vpc_name=vpc_config.vpc_name,
+ public_subnets=self.public_subnets,
+ private_subnets=self.private_subnets,
+ isolated_subnets=self.isolated_subnets,
+ availability_zones=vpc_config.availability_zones,
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/policy_generators/__init__.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/policy_generators/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/policy_generators/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/policy_generators/ec2_vpc.py b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/policy_generators/ec2_vpc.py
new file mode 100644
index 00000000..69bf68bb
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/infrastructure/lib/cms_common/policy_generators/ec2_vpc.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+# AWS Libraries
+
+# AWS Libraries
+from aws_cdk import Stack, aws_ec2, aws_iam
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ..constructs.vpc_construct import VpcConstruct
+
+
+def generate_ec2_vpc_policy(
+ self: Construct,
+ vpc_construct: VpcConstruct,
+ subnet_selection: aws_ec2.SubnetSelection,
+ authorized_service: str,
+) -> aws_iam.PolicyDocument:
+ return aws_iam.PolicyDocument(
+ statements=[
+ aws_iam.PolicyStatement(
+ effect=aws_iam.Effect.ALLOW,
+ actions=[
+ "ec2:CreateNetworkInterfacePermission",
+ ],
+ resources=[
+ Stack.of(self).format_arn(
+ partition=Stack.of(self).partition,
+ service="ec2",
+ region=Stack.of(self).region,
+ account=Stack.of(self).account,
+ resource="network-interface",
+ resource_name="*",
+ ),
+ ],
+ conditions={
+ "StringEquals": {
+ "ec2:Subnet": [
+ Stack.of(self).format_arn(
+ partition=Stack.of(self).partition,
+ service="ec2",
+ region=Stack.of(self).region,
+ account=Stack.of(self).account,
+ resource="subnet",
+ resource_name=subnet_id,
+ )
+ for subnet_id in vpc_construct.vpc.select_subnets( # type: ignore[union-attr]
+ subnet_selection
+ ).get(
+ "subnetIds"
+ )
+ ],
+ "ec2:AuthorizedService": authorized_service,
+ }
+ },
+ ),
+ aws_iam.PolicyStatement(
+ actions=[
+ "ec2:DescribeNetworkInterfaces",
+ "ec2:CreateNetworkInterface",
+ "ec2:DeleteNetworkInterface",
+ ],
+ effect=aws_iam.Effect.ALLOW,
+ resources=["*"],
+ ),
+ ]
+ )
diff --git a/source/modules/acdp/backstage/cdk/source/tests/__init__.py b/source/modules/acdp/backstage/cdk/source/tests/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/tests/conftest.py b/source/modules/acdp/backstage/cdk/source/tests/conftest.py
new file mode 100644
index 00000000..c559c317
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/tests/conftest.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# pylint: disable=W0611
+
+# Connected Mobility Solution on AWS
+from .infrastructure.fixtures.fixture_stack_templates import (
+ fixture_acdp_backstage_stack_template,
+ fixture_snapshot_json_with_matcher,
+)
diff --git a/source/modules/acdp/backstage/cdk/source/tests/fixtures/__init__.py b/source/modules/acdp/backstage/cdk/source/tests/fixtures/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/tests/fixtures/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/tests/infrastructure/__init__.py b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/tests/infrastructure/__snapshots__/test_snapshot/test_acdp_backstage_snapshot.json b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/__snapshots__/test_snapshot/test_acdp_backstage_snapshot.json
new file mode 100644
index 00000000..a92aa840
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/__snapshots__/test_snapshot/test_acdp_backstage_snapshot.json
@@ -0,0 +1,3755 @@
+{
+ "Mappings": {
+ "AWSCloudFrontPartitionHostedZoneIdMap": {
+ "aws": {
+ "zoneId": "Z2FDTNDATAQYW2"
+ },
+ "aws-cn": {
+ "zoneId": "Z3RFFRIM2A3IF5"
+ }
+ },
+ "Solution": {
+ "AssetsConfig": {
+ "S3AssetBucketName": "test-bucket-name",
+ "S3AssetKeyPrefix": "test-object-key-prefix"
+ }
+ },
+ "acdpbackstageauroradatabaseconstructauroraserverlessclusterRotationSingleUserSARMapping62943CC5": {
+ "aws": {
+ "applicationId": "test",
+ "semanticVersion": "1.1.367"
+ },
+ "aws-cn": {
+ "applicationId": "test",
+ "semanticVersion": "1.1.212"
+ },
+ "aws-us-gov": {
+ "applicationId": "test",
+ "semanticVersion": "1.1.93"
+ }
+ }
+ },
+ "Parameters": {
+ "AcdpUniqueId": {
+ "Default": "acdp",
+ "Description": "Name of the ACDP deployment",
+ "Type": "String"
+ },
+ "BootstrapVersion": {
+ "Default": "/cdk-bootstrap/hnb659fds/version",
+ "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
+ "Type": "AWS::SSM::Parameter::Value"
+ },
+ "VpcName": {
+ "Description": "name of the imported vpc",
+ "Type": "String"
+ }
+ },
+ "Resources": {
+ "AWS679f53fac002430cb0da5b7982bd22872D164C4C": {
+ "DependsOn": [
+ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"
+ ],
+ "Properties": {
+ "Code": {
+ "S3Bucket": "cdk-hnb659fds-assets-test-account-id-us-west-2",
+ "S3Key": "test"
+ },
+ "Handler": "index.handler",
+ "Role": {
+ "Fn::GetAtt": [
+ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
+ "Arn"
+ ]
+ },
+ "Runtime": "nodejs18.x",
+ "Timeout": 120
+ },
+ "Type": "AWS::Lambda::Function"
+ },
+ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": {
+ "Properties": {
+ "AssumeRolePolicyDocument": {
+ "Statement": [
+ {
+ "Action": "sts:AssumeRole",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "lambda.amazonaws.com"
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "ManagedPolicyArns": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+ ]
+ ]
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Role"
+ },
+ "acdpbackstageauroradatabaseconstructauroraserverlesscluster0FD1CD15": {
+ "DeletionPolicy": "Snapshot",
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "CopyTagsToSnapshot": true,
+ "DBClusterParameterGroupName": "default.aurora-postgresql13",
+ "DBSubnetGroupName": {
+ "Ref": "acdpbackstageauroradatabaseconstructauroraserverlessclusterSubnets00E2E32A"
+ },
+ "DeletionProtection": false,
+ "Engine": "aurora-postgresql",
+ "EngineMode": "serverless",
+ "EngineVersion": "13.9",
+ "MasterUserPassword": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:secretsmanager:",
+ {
+ "Ref": "acdpbackstageauroradatabaseconstructdatabasesecret557C9B62"
+ },
+ ":SecretString:password::}}"
+ ]
+ ]
+ },
+ "MasterUsername": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:secretsmanager:",
+ {
+ "Ref": "acdpbackstageauroradatabaseconstructdatabasesecret557C9B62"
+ },
+ ":SecretString:username::}}"
+ ]
+ ]
+ },
+ "StorageEncrypted": true,
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "VpcSecurityGroupIds": [
+ {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygroup40706936",
+ "GroupId"
+ ]
+ }
+ ]
+ },
+ "Type": "AWS::RDS::DBCluster",
+ "UpdateReplacePolicy": "Snapshot"
+ },
+ "acdpbackstageauroradatabaseconstructauroraserverlessclusterRotationSingleUser489CBB82": {
+ "DeletionPolicy": "Delete",
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Location": {
+ "ApplicationId": {
+ "Fn::FindInMap": [
+ "acdpbackstageauroradatabaseconstructauroraserverlessclusterRotationSingleUserSARMapping62943CC5",
+ {
+ "Ref": "AWS::Partition"
+ },
+ "applicationId"
+ ]
+ },
+ "SemanticVersion": {
+ "Fn::FindInMap": [
+ "acdpbackstageauroradatabaseconstructauroraserverlessclusterRotationSingleUserSARMapping62943CC5",
+ {
+ "Ref": "AWS::Partition"
+ },
+ "semanticVersion"
+ ]
+ }
+ },
+ "Parameters": {
+ "endpoint": {
+ "Fn::Join": [
+ "",
+ [
+ "https://secretsmanager.us-west-2.",
+ {
+ "Ref": "AWS::URLSuffix"
+ }
+ ]
+ ]
+ },
+ "excludeCharacters": " %+~`#$&*()|[]{}:;<>?!'/@\"\\",
+ "functionName": "tabaseconstructauroraserverlessclusterRotationSingleUserD8895971",
+ "vpcSecurityGroupIds": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygroup40706936",
+ "GroupId"
+ ]
+ },
+ "vpcSubnetIds": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/subnets/isolated/1}},{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/subnets/isolated/2}}"
+ ]
+ ]
+ }
+ },
+ "Tags": {
+ "Solutions:DeploymentUUID": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ },
+ "Type": "AWS::Serverless::Application",
+ "UpdateReplacePolicy": "Delete"
+ },
+ "acdpbackstageauroradatabaseconstructauroraserverlessclusterSubnets00E2E32A": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "DBSubnetGroupDescription": "Subnets for aurora-serverless-cluster database",
+ "SubnetIds": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/subnets/isolated/1}}"
+ ]
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/subnets/isolated/2}}"
+ ]
+ ]
+ }
+ ],
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ]
+ },
+ "Type": "AWS::RDS::DBSubnetGroup"
+ },
+ "acdpbackstageauroradatabaseconstructdatabasesecret557C9B62": {
+ "DeletionPolicy": "Delete",
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Description": {
+ "Fn::Join": [
+ "",
+ [
+ "Generated by the CDK for stack: ",
+ {
+ "Ref": "AWS::StackName"
+ }
+ ]
+ ]
+ },
+ "GenerateSecretString": {
+ "ExcludeCharacters": " %+~`#$&*()|[]{}:;<>?!'/@\"\\",
+ "GenerateStringKey": "password",
+ "PasswordLength": 30,
+ "SecretStringTemplate": "{\"username\":\"db_admin\"}"
+ },
+ "Name": {
+ "Fn::Join": [
+ "",
+ [
+ "/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/db_credentials"
+ ]
+ ]
+ },
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ]
+ },
+ "Type": "AWS::SecretsManager::Secret",
+ "UpdateReplacePolicy": "Delete"
+ },
+ "acdpbackstageauroradatabaseconstructdatabasesecretAttachment1583CE9A": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "SecretId": {
+ "Ref": "acdpbackstageauroradatabaseconstructdatabasesecret557C9B62"
+ },
+ "TargetId": {
+ "Ref": "acdpbackstageauroradatabaseconstructauroraserverlesscluster0FD1CD15"
+ },
+ "TargetType": "AWS::RDS::DBCluster"
+ },
+ "Type": "AWS::SecretsManager::SecretTargetAttachment"
+ },
+ "acdpbackstageauroradatabaseconstructdatabasesecretAttachmentPolicyE6FA7D24": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "ResourcePolicy": {
+ "Statement": [
+ {
+ "Action": "secretsmanager:DeleteSecret",
+ "Effect": "Deny",
+ "Principal": {
+ "AWS": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":iam::test-account-id:root"
+ ]
+ ]
+ }
+ },
+ "Resource": "*"
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "SecretId": {
+ "Ref": "acdpbackstageauroradatabaseconstructdatabasesecretAttachment1583CE9A"
+ }
+ },
+ "Type": "AWS::SecretsManager::ResourcePolicy"
+ },
+ "acdpbackstageauroradatabaseconstructdatabasesecretAttachmentRotationSchedule0A8062E2": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "RotationLambdaARN": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructauroraserverlessclusterRotationSingleUser489CBB82",
+ "Outputs.RotationLambdaARN"
+ ]
+ },
+ "RotationRules": {
+ "ScheduleExpression": "rate(90 days)"
+ },
+ "SecretId": {
+ "Ref": "acdpbackstageauroradatabaseconstructdatabasesecretAttachment1583CE9A"
+ }
+ },
+ "Type": "AWS::SecretsManager::RotationSchedule"
+ },
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygroup40706936": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "GroupDescription": "backstage/acdp-backstage/aurora-database-construct/database-security-group",
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "VpcId": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/vpcid}}"
+ ]
+ ]
+ }
+ },
+ "Type": "AWS::EC2::SecurityGroup"
+ },
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygroupfrombackstageacdpbackstageauroradatabaseconstructdatabasesecuritygroup833C86EBIndirectPort9AD5CBFC": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Description": "from backstageacdpbackstageauroradatabaseconstructdatabasesecuritygroup833C86EB:{IndirectPort}",
+ "FromPort": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructauroraserverlesscluster0FD1CD15",
+ "Endpoint.Port"
+ ]
+ },
+ "GroupId": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygroup40706936",
+ "GroupId"
+ ]
+ },
+ "IpProtocol": "tcp",
+ "SourceSecurityGroupId": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygroup40706936",
+ "GroupId"
+ ]
+ },
+ "ToPort": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructauroraserverlesscluster0FD1CD15",
+ "Endpoint.Port"
+ ]
+ }
+ },
+ "Type": "AWS::EC2::SecurityGroupIngress"
+ },
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygroupfrombackstageacdpbackstagebackstagecontainerconstructfargatesecuritygroup77FE88265432E027E757": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Description": "Allow ingress from fargate",
+ "FromPort": 5432,
+ "GroupId": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygroup40706936",
+ "GroupId"
+ ]
+ },
+ "IpProtocol": "tcp",
+ "SourceSecurityGroupId": {
+ "Fn::GetAtt": [
+ "acdpbackstagebackstagecontainerconstructfargatesecuritygroupB778CC94",
+ "GroupId"
+ ]
+ },
+ "ToPort": 5432
+ },
+ "Type": "AWS::EC2::SecurityGroupIngress"
+ },
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygrouptobackstageacdpbackstageauroradatabaseconstructdatabasesecuritygroup833C86EBIndirectPort6D302B2C": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Description": "to backstageacdpbackstageauroradatabaseconstructdatabasesecuritygroup833C86EB:{IndirectPort}",
+ "DestinationSecurityGroupId": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygroup40706936",
+ "GroupId"
+ ]
+ },
+ "FromPort": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructauroraserverlesscluster0FD1CD15",
+ "Endpoint.Port"
+ ]
+ },
+ "GroupId": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructdatabasesecuritygroup40706936",
+ "GroupId"
+ ]
+ },
+ "IpProtocol": "tcp",
+ "ToPort": {
+ "Fn::GetAtt": [
+ "acdpbackstageauroradatabaseconstructauroraserverlesscluster0FD1CD15",
+ "Endpoint.Port"
+ ]
+ }
+ },
+ "Type": "AWS::EC2::SecurityGroupEgress"
+ },
+ "acdpbackstagebackstagecontainerconstructbackendsecretACD7C77E": {
+ "DeletionPolicy": "Delete",
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Description": "Backend secret",
+ "GenerateSecretString": {},
+ "Name": {
+ "Fn::Join": [
+ "",
+ [
+ "/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/backend-secret"
+ ]
+ ]
+ },
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ]
+ },
+ "Type": "AWS::SecretsManager::Secret",
+ "UpdateReplacePolicy": "Delete"
+ },
+ "acdpbackstagebackstagecontainerconstructcontainerloggroup00CD59BA": {
+ "DeletionPolicy": "Retain",
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "KmsKeyId": {
+ "Fn::GetAtt": [
+ "acdpbackstagebackstagecontainerconstructcontainerloggroupkmskeyF9390F98",
+ "Arn"
+ ]
+ },
+ "RetentionInDays": 90,
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ]
+ },
+ "Type": "AWS::Logs::LogGroup",
+ "UpdateReplacePolicy": "Retain"
+ },
+ "acdpbackstagebackstagecontainerconstructcontainerloggroupkmskeyF9390F98": {
+ "DeletionPolicy": "Retain",
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "EnableKeyRotation": true,
+ "KeyPolicy": {
+ "Statement": [
+ {
+ "Action": "kms:*",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":iam::test-account-id:root"
+ ]
+ ]
+ }
+ },
+ "Resource": "*"
+ },
+ {
+ "Action": [
+ "kms:Encrypt",
+ "kms:Decrypt",
+ "kms:GenerateDataKey"
+ ],
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "logs.us-west-2.amazonaws.com"
+ },
+ "Resource": "*"
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ]
+ },
+ "Type": "AWS::KMS::Key",
+ "UpdateReplacePolicy": "Retain"
+ },
+ "acdpbackstagebackstagecontainerconstructecsclusterFDB21F65": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "ClusterSettings": [
+ {
+ "Name": "containerInsights",
+ "Value": "enabled"
+ }
+ ],
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ]
+ },
+ "Type": "AWS::ECS::Cluster"
+ },
+ "acdpbackstagebackstagecontainerconstructfargatesecuritygroupB778CC94": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "GroupDescription": "backstage/acdp-backstage/backstage-container-construct/fargate-security-group",
+ "SecurityGroupEgress": [
+ {
+ "CidrIp": "0.0.0.0/0",
+ "Description": "Allow all outbound traffic by default",
+ "IpProtocol": "-1"
+ }
+ ],
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "VpcId": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/vpcid}}"
+ ]
+ ]
+ }
+ },
+ "Type": "AWS::EC2::SecurityGroup"
+ },
+ "acdpbackstagebackstagecontainerconstructfargatesecuritygroupfrombackstageacdpbackstageloadbalancerconstructalbsecuritygroup064211D7443DE60CC5B": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Description": "alb security group to fargate security group connection",
+ "FromPort": 443,
+ "GroupId": {
+ "Fn::GetAtt": [
+ "acdpbackstagebackstagecontainerconstructfargatesecuritygroupB778CC94",
+ "GroupId"
+ ]
+ },
+ "IpProtocol": "tcp",
+ "SourceSecurityGroupId": {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructalbsecuritygroup1208C47D",
+ "GroupId"
+ ]
+ },
+ "ToPort": 443
+ },
+ "Type": "AWS::EC2::SecurityGroupIngress"
+ },
+ "acdpbackstagebackstagecontainerconstructfargatesecuritygroupfrombackstageacdpbackstageloadbalancerconstructalbsecuritygroup064211D780800D19E88B": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Description": "Load balancer to target",
+ "FromPort": 8080,
+ "GroupId": {
+ "Fn::GetAtt": [
+ "acdpbackstagebackstagecontainerconstructfargatesecuritygroupB778CC94",
+ "GroupId"
+ ]
+ },
+ "IpProtocol": "tcp",
+ "SourceSecurityGroupId": {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructalbsecuritygroup1208C47D",
+ "GroupId"
+ ]
+ },
+ "ToPort": 8080
+ },
+ "Type": "AWS::EC2::SecurityGroupIngress"
+ },
+ "acdpbackstagebackstagecontainerconstructfargateserviceServiceDABFAF3F": {
+ "DependsOn": [
+ "acdpbackstagebackstagecontainerconstructtaskdefinitionroleDefaultPolicyE666B526",
+ "acdpbackstagebackstagecontainerconstructtaskdefinitionrole8D0F5D23",
+ "acdpbackstageloadbalancerconstructapplicationloadbalancerlistenerfleetGroupF9A486B0",
+ "acdpbackstageloadbalancerconstructapplicationloadbalancerlistenerC3A53880",
+ "acdpbackstageloadbalancerconstructlistenerrule6E295068",
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Cluster": {
+ "Ref": "acdpbackstagebackstagecontainerconstructecsclusterFDB21F65"
+ },
+ "DeploymentConfiguration": {
+ "Alarms": {
+ "AlarmNames": [],
+ "Enable": false,
+ "Rollback": false
+ },
+ "MaximumPercent": 200,
+ "MinimumHealthyPercent": 50
+ },
+ "DesiredCount": 2,
+ "EnableECSManagedTags": false,
+ "HealthCheckGracePeriodSeconds": 60,
+ "LaunchType": "FARGATE",
+ "LoadBalancers": [
+ {
+ "ContainerName": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Ref": "AWS::StackName"
+ },
+ "-backend"
+ ]
+ ]
+ },
+ "ContainerPort": 8080,
+ "TargetGroupArn": {
+ "Ref": "acdpbackstageloadbalancerconstructapplicationloadbalancerlistenerfleetGroupF9A486B0"
+ }
+ }
+ ],
+ "NetworkConfiguration": {
+ "AwsvpcConfiguration": {
+ "AssignPublicIp": "DISABLED",
+ "SecurityGroups": [
+ {
+ "Fn::GetAtt": [
+ "acdpbackstagebackstagecontainerconstructfargatesecuritygroupB778CC94",
+ "GroupId"
+ ]
+ }
+ ],
+ "Subnets": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/subnets/private/1}}"
+ ]
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/subnets/private/2}}"
+ ]
+ ]
+ }
+ ]
+ }
+ },
+ "ServiceName": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Ref": "AWS::StackName"
+ },
+ "-fargate-service"
+ ]
+ ]
+ },
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "TaskDefinition": {
+ "Ref": "acdpbackstagebackstagecontainerconstructfargatetaskdefinition26D75CF1"
+ }
+ },
+ "Type": "AWS::ECS::Service"
+ },
+ "acdpbackstagebackstagecontainerconstructfargatetaskdefinition26D75CF1": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "ContainerDefinitions": [
+ {
+ "Environment": [
+ {
+ "Name": "SOLUTION_NAME",
+ "Value": "test-solution-name"
+ },
+ {
+ "Name": "SOLUTION_VERSION",
+ "Value": "v0.0.0"
+ },
+ {
+ "Name": "WEB_HOSTNAME",
+ "Value": "dummy"
+ },
+ {
+ "Name": "BACKEND_HOSTNAME",
+ "Value": "dummy"
+ },
+ {
+ "Name": "NODE_ENV",
+ "Value": "production"
+ },
+ {
+ "Name": "COGNITO_USERPOOL_ID",
+ "Value": {
+ "Ref": "acdpbackstagecognitoconstructuserpool9E33B8EE"
+ }
+ },
+ {
+ "Name": "LOG_LEVEL",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/log-level}}"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "USER_AGENT_STRING",
+ "Value": "AWSSOLUTION/test-solution-id/test-solution-version AWSSOLUTION-CAPABILITY/test-capability-id/test-solution-version"
+ },
+ {
+ "Name": "COGNITO_CLIENT_ID",
+ "Value": {
+ "Ref": "acdpbackstagecognitoconstructuserpooloidcclient845AC868"
+ }
+ }
+ ],
+ "Essential": true,
+ "Image": {
+ "Fn::Join": [
+ "",
+ [
+ "test-account-id.dkr.ecr.us-west-2.",
+ {
+ "Ref": "AWS::URLSuffix"
+ },
+ "/{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/ecr-repository/name}}:DUMMY"
+ ]
+ ]
+ },
+ "LogConfiguration": {
+ "LogDriver": "awslogs",
+ "Options": {
+ "awslogs-group": {
+ "Ref": "acdpbackstagebackstagecontainerconstructcontainerloggroup00CD59BA"
+ },
+ "awslogs-region": "us-west-2",
+ "awslogs-stream-prefix": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Ref": "AWS::StackName"
+ },
+ "-logs"
+ ]
+ ]
+ }
+ }
+ },
+ "Name": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Ref": "AWS::StackName"
+ },
+ "-backend"
+ ]
+ ]
+ },
+ "PortMappings": [
+ {
+ "ContainerPort": 8080,
+ "Protocol": "tcp"
+ }
+ ],
+ "Secrets": [
+ {
+ "Name": "BACKSTAGE_NAME",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/name"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "BACKSTAGE_ORG",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/organization"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "POSTGRES_USER",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Ref": "acdpbackstageauroradatabaseconstructdatabasesecret557C9B62"
+ },
+ ":username::"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "POSTGRES_PASSWORD",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Ref": "acdpbackstageauroradatabaseconstructdatabasesecret557C9B62"
+ },
+ ":password::"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "POSTGRES_HOST",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Ref": "acdpbackstageauroradatabaseconstructdatabasesecret557C9B62"
+ },
+ ":host::"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "POSTGRES_PORT",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Ref": "acdpbackstageauroradatabaseconstructdatabasesecret557C9B62"
+ },
+ ":port::"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "BACKEND_SECRET",
+ "ValueFrom": {
+ "Ref": "acdpbackstagebackstagecontainerconstructbackendsecretACD7C77E"
+ }
+ },
+ {
+ "Name": "REGIONAL_ASSET_BUCKET_NAME",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/asset-bucket/name"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "REGIONAL_ASSET_BUCKET_REGION",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/asset-bucket/region"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "REGIONAL_ASSET_BUCKET_BACKSTAGE_TEMPLATE_KEY_PREFIX",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/backstage-template-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "REGIONAL_ASSET_BUCKET_DISCOVERY_REFRESH_FREQ",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/discovery-refresh-frequency-mins"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "REGIONAL_ASSET_BUCKET_BUILDSPEC_KEY_PREFIX",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/buildspec-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "LOCAL_ASSET_BUCKET_NAME",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/asset-bucket/name"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "LOCAL_ASSET_BUCKET_REGION",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/asset-bucket/region"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "LOCAL_ASSET_BUCKET_ROOT_KEY",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/root-s3-key"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "LOCAL_ASSET_BUCKET_BACKSTAGE_USER_PROVIDED_TEMPLATE_KEY_PREFIX",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/backstage-custom-template-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "LOCAL_ASSET_BUCKET_BACKSTAGE_DEFAULT_TEMPLATE_KEY_PREFIX",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/backstage-default-template-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "LOCAL_ASSET_BUCKET_CATALOG_KEY_PREFIX",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/catalog-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "LOCAL_ASSET_BUCKET_TECHDOCS_KEY_PREFIX",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/techdocs-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "LOCAL_ASSET_BUCKET_DISCOVERY_REFRESH_FREQ",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/discovery-refresh-frequency-mins"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "CODEBUILD_PROJECT_ARN",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/codebuild-project/arn"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "ACDP_BUILD_CONFIG_SSM_PREFIX",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/acdp-build/build-parameters"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "TARGET_ACCOUNT_ID",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-targets/default/account-id"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "TARGET_REGION",
+ "ValueFrom": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-targets/default/region"
+ ]
+ ]
+ }
+ }
+ ]
+ }
+ ],
+ "Cpu": "1024",
+ "EphemeralStorage": {
+ "SizeInGiB": 30
+ },
+ "ExecutionRoleArn": {
+ "Fn::GetAtt": [
+ "acdpbackstagebackstagecontainerconstructtaskdefinitionrole8D0F5D23",
+ "Arn"
+ ]
+ },
+ "Family": {
+ "Ref": "AWS::StackName"
+ },
+ "Memory": "2048",
+ "NetworkMode": "awsvpc",
+ "RequiresCompatibilities": [
+ "FARGATE"
+ ],
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "TaskRoleArn": {
+ "Fn::GetAtt": [
+ "acdpbackstagebackstagecontainerconstructtaskdefinitionrole8D0F5D23",
+ "Arn"
+ ]
+ }
+ },
+ "Type": "AWS::ECS::TaskDefinition"
+ },
+ "acdpbackstagebackstagecontainerconstructssmbackstagetargetaccountidcreatessmparam6682919B": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Description": "Backstage Deployment Target Account Id",
+ "Name": {
+ "Fn::Join": [
+ "",
+ [
+ "/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-targets/default/account-id"
+ ]
+ ]
+ },
+ "Tags": {
+ "Solutions:DeploymentUUID": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ },
+ "Type": "String",
+ "Value": "test-account-id"
+ },
+ "Type": "AWS::SSM::Parameter"
+ },
+ "acdpbackstagebackstagecontainerconstructssmbackstagetargetregioncreatessmparam5DB4BF2E": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Description": "Backstage Deployment Target Region",
+ "Name": {
+ "Fn::Join": [
+ "",
+ [
+ "/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-targets/default/region"
+ ]
+ ]
+ },
+ "Tags": {
+ "Solutions:DeploymentUUID": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ },
+ "Type": "String",
+ "Value": "us-west-2"
+ },
+ "Type": "AWS::SSM::Parameter"
+ },
+ "acdpbackstagebackstagecontainerconstructtaskdefinitionrole8D0F5D23": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "AssumeRolePolicyDocument": {
+ "Statement": [
+ {
+ "Action": "sts:AssumeRole",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "ecs-tasks.amazonaws.com"
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "Policies": [
+ {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "ssm:GetParameter",
+ "ssm:PutParameter"
+ ],
+ "Effect": "Allow",
+ "Resource": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/acdp-build"
+ ]
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/acdp-build/*"
+ ]
+ ]
+ }
+ ]
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "ssm-policy"
+ },
+ {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "s3:GetBucketAcl",
+ "s3:GetBucketLocation",
+ "s3:GetBucketVersioning",
+ "s3:GetObject",
+ "s3:GetObjectAcl",
+ "s3:GetObjectAttributes",
+ "s3:GetObjectVersion",
+ "s3:GetObjectVersionAcl",
+ "s3:GetObjectVersionTagging",
+ "s3:ListAllMyBuckets",
+ "s3:ListBucket",
+ "s3:ListBucketVersions"
+ ],
+ "Effect": "Allow",
+ "Resource": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":s3:::{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/asset-bucket/name}}"
+ ]
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":s3:::{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/asset-bucket/name}}/*"
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "Action": [
+ "s3:GetBucketAcl",
+ "s3:GetBucketLocation",
+ "s3:GetBucketVersioning",
+ "s3:GetObject",
+ "s3:GetObjectAcl",
+ "s3:GetObjectAttributes",
+ "s3:GetObjectVersion",
+ "s3:GetObjectVersionAcl",
+ "s3:GetObjectVersionTagging",
+ "s3:ListAllMyBuckets",
+ "s3:ListBucket",
+ "s3:ListBucketVersions",
+ "s3:PutObject",
+ "s3:DeleteObject",
+ "s3:DeleteObjectVersion"
+ ],
+ "Effect": "Allow",
+ "Resource": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":s3:::{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/asset-bucket/name}}"
+ ]
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":s3:::{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/asset-bucket/name}}/{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/root-s3-key}}/*"
+ ]
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":s3:::{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/asset-bucket/name}}/{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/default-assets-prefix}}/*"
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "Action": [
+ "kms:GenerateDataKey",
+ "kms:Decrypt",
+ "kms:Encrypt"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/asset-bucket/key-arn}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "s3-policy"
+ },
+ {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "cognito-idp:DescribeUserPool",
+ "cognito-idp:DescribeUserPoolClient"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":cognito-idp:us-west-2:test-account-id:userpool/",
+ {
+ "Ref": "acdpbackstagecognitoconstructuserpool9E33B8EE"
+ }
+ ]
+ ]
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "cognito-idp-policy"
+ },
+ {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "codebuild:StartBuild",
+ "codebuild:BatchGetProjects",
+ "codebuild:BatchGetBuilds",
+ "codebuild:ListBuildsForProject"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":codebuild:us-west-2:test-account-id:project/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "-*"
+ ]
+ ]
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "codebuild-policy"
+ }
+ ],
+ "RoleName": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "-us-west-2-backstage-task"
+ ]
+ ]
+ },
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Role"
+ },
+ "acdpbackstagebackstagecontainerconstructtaskdefinitionroleDefaultPolicyE666B526": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "ecr:BatchCheckLayerAvailability",
+ "ecr:GetDownloadUrlForLayer",
+ "ecr:BatchGetImage"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ecr:us-west-2:test-account-id:repository/{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/ecr-repository/name}}"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": "ecr:GetAuthorizationToken",
+ "Effect": "Allow",
+ "Resource": "*"
+ },
+ {
+ "Action": [
+ "logs:CreateLogStream",
+ "logs:PutLogEvents"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::GetAtt": [
+ "acdpbackstagebackstagecontainerconstructcontainerloggroup00CD59BA",
+ "Arn"
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/name"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/organization"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "secretsmanager:GetSecretValue",
+ "secretsmanager:DescribeSecret"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Ref": "acdpbackstageauroradatabaseconstructdatabasesecret557C9B62"
+ }
+ },
+ {
+ "Action": [
+ "secretsmanager:GetSecretValue",
+ "secretsmanager:DescribeSecret"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Ref": "acdpbackstagebackstagecontainerconstructbackendsecretACD7C77E"
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/asset-bucket/name"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/asset-bucket/region"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/backstage-template-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/discovery-refresh-frequency-mins"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/regional/buildspec-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/asset-bucket/name"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/asset-bucket/region"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/root-s3-key"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/backstage-custom-template-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/backstage-default-template-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/catalog-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/techdocs-key-prefix"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/acdp-asset-config/local/discovery-refresh-frequency-mins"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/codebuild-project/arn"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/acdp-build/build-parameters"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-targets/default/account-id"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": [
+ "ssm:DescribeParameters",
+ "ssm:GetParameters",
+ "ssm:GetParameter",
+ "ssm:GetParameterHistory"
+ ],
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":ssm:us-west-2:test-account-id:parameter/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-targets/default/region"
+ ]
+ ]
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "acdpbackstagebackstagecontainerconstructtaskdefinitionroleDefaultPolicyE666B526",
+ "Roles": [
+ {
+ "Ref": "acdpbackstagebackstagecontainerconstructtaskdefinitionrole8D0F5D23"
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Policy"
+ },
+ "acdpbackstagecdklambdasvpcconstructsecuritygroupA7B11D31": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "GroupDescription": "backstage/acdp-backstage/cdk-lambdas-vpc-construct/security-group",
+ "SecurityGroupEgress": [
+ {
+ "CidrIp": "0.0.0.0/0",
+ "Description": "Allow all outbound traffic by default",
+ "IpProtocol": "-1"
+ }
+ ],
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "VpcId": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/vpcid}}"
+ ]
+ ]
+ }
+ },
+ "Type": "AWS::EC2::SecurityGroup"
+ },
+ "acdpbackstagecognitoconstructadminuser30A395BB": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "DesiredDeliveryMediums": [
+ "EMAIL"
+ ],
+ "ForceAliasCreation": true,
+ "UserAttributes": [
+ {
+ "Name": "email",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/admin-email}}"
+ ]
+ ]
+ }
+ },
+ {
+ "Name": "email_verified",
+ "Value": "true"
+ }
+ ],
+ "UserPoolId": {
+ "Ref": "acdpbackstagecognitoconstructuserpool9E33B8EE"
+ },
+ "Username": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/backstage/admin-username}}"
+ ]
+ ]
+ }
+ },
+ "Type": "AWS::Cognito::UserPoolUser"
+ },
+ "acdpbackstagecognitoconstructuserpool9E33B8EE": {
+ "DeletionPolicy": "Retain",
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "AccountRecoverySetting": {
+ "RecoveryMechanisms": [
+ {
+ "Name": "verified_email",
+ "Priority": 1
+ }
+ ]
+ },
+ "AdminCreateUserConfig": {
+ "AllowAdminCreateUserOnly": true,
+ "InviteMessageTemplate": {
+ "EmailMessage": "\nHello {username}, you have been invited to join Connected Mobility Solution - Backstage.
\nhttps://dummy\n
\n\nPlease sign in using the temporary credentials below:
\n
\nUsername: {username}\nPassword: {####}\n
\n\n",
+ "EmailSubject": "Invite to join Connected Mobility Solution - Backstage!",
+ "SMSMessage": "Hello {username}, your temporary password for Connected Mobility Solution - Backstage is {####}"
+ }
+ },
+ "AliasAttributes": [
+ "email",
+ "preferred_username"
+ ],
+ "AutoVerifiedAttributes": [
+ "email"
+ ],
+ "DeviceConfiguration": {
+ "ChallengeRequiredOnNewDevice": true,
+ "DeviceOnlyRememberedOnUserPrompt": true
+ },
+ "EnabledMfas": [
+ "SOFTWARE_TOKEN_MFA"
+ ],
+ "MfaConfiguration": "ON",
+ "Policies": {
+ "PasswordPolicy": {
+ "MinimumLength": 12,
+ "RequireLowercase": true,
+ "RequireNumbers": true,
+ "RequireSymbols": true,
+ "RequireUppercase": true,
+ "TemporaryPasswordValidityDays": 1
+ }
+ },
+ "Schema": [
+ {
+ "Mutable": false,
+ "Name": "email",
+ "Required": true
+ },
+ {
+ "Mutable": true,
+ "Name": "name",
+ "Required": true
+ },
+ {
+ "Mutable": true,
+ "Name": "preferred_username",
+ "Required": false
+ }
+ ],
+ "SmsVerificationMessage": "Connected Mobility Solution - Backstage\nYour verification code is {####}",
+ "UserPoolAddOns": {
+ "AdvancedSecurityMode": "ENFORCED"
+ },
+ "UserPoolTags": {
+ "Solutions:DeploymentUUID": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ },
+ "VerificationMessageTemplate": {
+ "DefaultEmailOption": "CONFIRM_WITH_LINK",
+ "EmailMessageByLink": "Thank you for signing up!\nClick here to verify your e-mail: {##Verify Email##}",
+ "EmailSubjectByLink": "Connected Mobility Solution - Backstage - Verify your email",
+ "SmsMessage": "Connected Mobility Solution - Backstage\nYour verification code is {####}"
+ }
+ },
+ "Type": "AWS::Cognito::UserPool",
+ "UpdateReplacePolicy": "Retain"
+ },
+ "acdpbackstagecognitoconstructuserpooloidcclient845AC868": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "AccessTokenValidity": 60,
+ "AllowedOAuthFlows": [
+ "code"
+ ],
+ "AllowedOAuthFlowsUserPoolClient": true,
+ "AllowedOAuthScopes": [
+ "openid"
+ ],
+ "AuthSessionValidity": 3,
+ "CallbackURLs": [
+ "https://dummy/api/auth/cognito/handler/frame",
+ "https://dummy/oauth2/idpresponse"
+ ],
+ "EnableTokenRevocation": true,
+ "GenerateSecret": true,
+ "IdTokenValidity": 60,
+ "PreventUserExistenceErrors": "ENABLED",
+ "RefreshTokenValidity": 120,
+ "SupportedIdentityProviders": [
+ "COGNITO"
+ ],
+ "TokenValidityUnits": {
+ "AccessToken": "minutes",
+ "IdToken": "minutes",
+ "RefreshToken": "minutes"
+ },
+ "UserPoolId": {
+ "Ref": "acdpbackstagecognitoconstructuserpool9E33B8EE"
+ }
+ },
+ "Type": "AWS::Cognito::UserPoolClient"
+ },
+ "acdpbackstagecognitoconstructuserpooluserpooldomain9AB3C450": {
+ "DependsOn": [
+ "acdpbackstageloadbalancerconstructrootarecordE1828906",
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "CustomDomainConfig": {
+ "CertificateArn": {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorResource7E920D16",
+ "Arn"
+ ]
+ }
+ },
+ "Domain": "auth.dummy",
+ "UserPoolId": {
+ "Ref": "acdpbackstagecognitoconstructuserpool9E33B8EE"
+ }
+ },
+ "Type": "AWS::Cognito::UserPoolDomain"
+ },
+ "acdpbackstagecognitoconstructuserpooluserpooldomainCloudFrontDomainName2A64FD1E": {
+ "DeletionPolicy": "Delete",
+ "DependsOn": [
+ "acdpbackstagecognitoconstructuserpooluserpooldomainCloudFrontDomainNameCustomResourcePolicyC20B670E",
+ "acdpbackstageloadbalancerconstructrootarecordE1828906",
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Create": {
+ "Fn::Join": [
+ "",
+ [
+ "{\"service\":\"CognitoIdentityServiceProvider\",\"action\":\"describeUserPoolDomain\",\"parameters\":{\"Domain\":\"",
+ {
+ "Ref": "acdpbackstagecognitoconstructuserpooluserpooldomain9AB3C450"
+ },
+ "\"},\"physicalResourceId\":{\"id\":\"",
+ {
+ "Ref": "acdpbackstagecognitoconstructuserpooluserpooldomain9AB3C450"
+ },
+ "\"}}"
+ ]
+ ]
+ },
+ "InstallLatestAwsSdk": false,
+ "ServiceToken": {
+ "Fn::GetAtt": [
+ "AWS679f53fac002430cb0da5b7982bd22872D164C4C",
+ "Arn"
+ ]
+ },
+ "Update": {
+ "Fn::Join": [
+ "",
+ [
+ "{\"service\":\"CognitoIdentityServiceProvider\",\"action\":\"describeUserPoolDomain\",\"parameters\":{\"Domain\":\"",
+ {
+ "Ref": "acdpbackstagecognitoconstructuserpooluserpooldomain9AB3C450"
+ },
+ "\"},\"physicalResourceId\":{\"id\":\"",
+ {
+ "Ref": "acdpbackstagecognitoconstructuserpooluserpooldomain9AB3C450"
+ },
+ "\"}}"
+ ]
+ ]
+ }
+ },
+ "Type": "Custom::UserPoolCloudFrontDomainName",
+ "UpdateReplacePolicy": "Delete"
+ },
+ "acdpbackstagecognitoconstructuserpooluserpooldomainCloudFrontDomainNameCustomResourcePolicyC20B670E": {
+ "DependsOn": [
+ "acdpbackstageloadbalancerconstructrootarecordE1828906",
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": "cognito-idp:DescribeUserPoolDomain",
+ "Effect": "Allow",
+ "Resource": "*"
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "acdpbackstagecognitoconstructuserpooluserpooldomainCloudFrontDomainNameCustomResourcePolicyC20B670E",
+ "Roles": [
+ {
+ "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Policy"
+ },
+ "acdpbackstageloadbalancerconstructalbaccesslogsbucket46F5BBD7": {
+ "DeletionPolicy": "Retain",
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "BucketEncryption": {
+ "ServerSideEncryptionConfiguration": [
+ {
+ "ServerSideEncryptionByDefault": {
+ "SSEAlgorithm": "AES256"
+ }
+ }
+ ]
+ },
+ "PublicAccessBlockConfiguration": {
+ "BlockPublicAcls": true,
+ "BlockPublicPolicy": true,
+ "IgnorePublicAcls": true,
+ "RestrictPublicBuckets": true
+ },
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "VersioningConfiguration": {
+ "Status": "Enabled"
+ }
+ },
+ "Type": "AWS::S3::Bucket",
+ "UpdateReplacePolicy": "Retain"
+ },
+ "acdpbackstageloadbalancerconstructalbaccesslogsbucketPolicyE3AC6333": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Bucket": {
+ "Ref": "acdpbackstageloadbalancerconstructalbaccesslogsbucket46F5BBD7"
+ },
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": "s3:*",
+ "Condition": {
+ "Bool": {
+ "aws:SecureTransport": "false"
+ }
+ },
+ "Effect": "Deny",
+ "Principal": {
+ "AWS": "*"
+ },
+ "Resource": [
+ {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructalbaccesslogsbucket46F5BBD7",
+ "Arn"
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructalbaccesslogsbucket46F5BBD7",
+ "Arn"
+ ]
+ },
+ "/*"
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "Action": "s3:PutObject",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ "test"
+ ]
+ ]
+ }
+ },
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructalbaccesslogsbucket46F5BBD7",
+ "Arn"
+ ]
+ },
+ "/backstage-alb/AWSLogs/test-account-id/*"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": "s3:PutObject",
+ "Condition": {
+ "StringEquals": {
+ "s3:x-amz-acl": "bucket-owner-full-control"
+ }
+ },
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "delivery.logs.amazonaws.com"
+ },
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructalbaccesslogsbucket46F5BBD7",
+ "Arn"
+ ]
+ },
+ "/backstage-alb/AWSLogs/test-account-id/*"
+ ]
+ ]
+ }
+ },
+ {
+ "Action": "s3:GetBucketAcl",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "delivery.logs.amazonaws.com"
+ },
+ "Resource": {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructalbaccesslogsbucket46F5BBD7",
+ "Arn"
+ ]
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ }
+ },
+ "Type": "AWS::S3::BucketPolicy"
+ },
+ "acdpbackstageloadbalancerconstructalbsecuritygroup1208C47D": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "GroupDescription": "backstage/acdp-backstage/load-balancer-construct/alb-security-group",
+ "SecurityGroupEgress": [
+ {
+ "CidrIp": "0.0.0.0/0",
+ "Description": "Allow all outbound traffic by default",
+ "IpProtocol": "-1"
+ }
+ ],
+ "SecurityGroupIngress": [
+ {
+ "CidrIp": "0.0.0.0/0",
+ "Description": "Allow from anyone on port 443",
+ "FromPort": 443,
+ "IpProtocol": "tcp",
+ "ToPort": 443
+ }
+ ],
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "VpcId": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/vpcid}}"
+ ]
+ ]
+ }
+ },
+ "Type": "AWS::EC2::SecurityGroup"
+ },
+ "acdpbackstageloadbalancerconstructapplicationloadbalancer72C84123": {
+ "DependsOn": [
+ "acdpbackstageloadbalancerconstructalbaccesslogsbucketPolicyE3AC6333",
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "LoadBalancerAttributes": [
+ {
+ "Key": "deletion_protection.enabled",
+ "Value": "false"
+ },
+ {
+ "Key": "routing.http.drop_invalid_header_fields.enabled",
+ "Value": "true"
+ },
+ {
+ "Key": "access_logs.s3.enabled",
+ "Value": "true"
+ },
+ {
+ "Key": "access_logs.s3.bucket",
+ "Value": {
+ "Ref": "acdpbackstageloadbalancerconstructalbaccesslogsbucket46F5BBD7"
+ }
+ },
+ {
+ "Key": "access_logs.s3.prefix",
+ "Value": "backstage-alb"
+ }
+ ],
+ "Name": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Ref": "AWS::StackName"
+ },
+ "-alb"
+ ]
+ ]
+ },
+ "Scheme": "internet-facing",
+ "SecurityGroups": [
+ {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructalbsecuritygroup1208C47D",
+ "GroupId"
+ ]
+ }
+ ],
+ "Subnets": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/subnets/public/1}}"
+ ]
+ ]
+ },
+ {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/subnets/public/2}}"
+ ]
+ ]
+ }
+ ],
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "Type": "application"
+ },
+ "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer"
+ },
+ "acdpbackstageloadbalancerconstructapplicationloadbalancerlistenerC3A53880": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Certificates": [
+ {
+ "CertificateArn": {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorResource39D2FE55",
+ "Arn"
+ ]
+ }
+ }
+ ],
+ "DefaultActions": [
+ {
+ "TargetGroupArn": {
+ "Ref": "acdpbackstageloadbalancerconstructapplicationloadbalancerlistenerfleetGroupF9A486B0"
+ },
+ "Type": "forward"
+ }
+ ],
+ "LoadBalancerArn": {
+ "Ref": "acdpbackstageloadbalancerconstructapplicationloadbalancer72C84123"
+ },
+ "Port": 443,
+ "Protocol": "HTTPS",
+ "SslPolicy": "ELBSecurityPolicy-TLS13-1-2-Res-2021-06"
+ },
+ "Type": "AWS::ElasticLoadBalancingV2::Listener"
+ },
+ "acdpbackstageloadbalancerconstructapplicationloadbalancerlistenerfleetGroupF9A486B0": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Port": 443,
+ "Protocol": "HTTP",
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "TargetGroupAttributes": [
+ {
+ "Key": "stickiness.enabled",
+ "Value": "false"
+ }
+ ],
+ "TargetType": "ip",
+ "VpcId": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/vpc/",
+ {
+ "Ref": "VpcName"
+ },
+ "/vpcid}}"
+ ]
+ ]
+ }
+ },
+ "Type": "AWS::ElasticLoadBalancingV2::TargetGroup"
+ },
+ "acdpbackstageloadbalancerconstructcognitoarecordC06E2DFE": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "AliasTarget": {
+ "DNSName": {
+ "Fn::GetAtt": [
+ "acdpbackstagecognitoconstructuserpooluserpooldomainCloudFrontDomainName2A64FD1E",
+ "DomainDescription.CloudFrontDistribution"
+ ]
+ },
+ "HostedZoneId": {
+ "Fn::FindInMap": [
+ "AWSCloudFrontPartitionHostedZoneIdMap",
+ {
+ "Ref": "AWS::Partition"
+ },
+ "zoneId"
+ ]
+ }
+ },
+ "HostedZoneId": "DUMMY",
+ "Name": "auth.dummy.",
+ "Type": "A"
+ },
+ "Type": "AWS::Route53::RecordSet"
+ },
+ "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorFunction356EC2BA": {
+ "DependsOn": [
+ "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorFunctionServiceRoleDefaultPolicy70B2B3EE",
+ "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorFunctionServiceRole53C01DAE",
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Code": {
+ "S3Bucket": "cdk-hnb659fds-assets-test-account-id-us-west-2",
+ "S3Key": "test"
+ },
+ "Handler": "index.certificateRequestHandler",
+ "Role": {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorFunctionServiceRole53C01DAE",
+ "Arn"
+ ]
+ },
+ "Runtime": "nodejs18.x",
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "Timeout": 900
+ },
+ "Type": "AWS::Lambda::Function"
+ },
+ "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorFunctionServiceRole53C01DAE": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "AssumeRolePolicyDocument": {
+ "Statement": [
+ {
+ "Action": "sts:AssumeRole",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "lambda.amazonaws.com"
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "ManagedPolicyArns": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+ ]
+ ]
+ }
+ ],
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Role"
+ },
+ "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorFunctionServiceRoleDefaultPolicy70B2B3EE": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "acm:RequestCertificate",
+ "acm:DescribeCertificate",
+ "acm:DeleteCertificate",
+ "acm:AddTagsToCertificate"
+ ],
+ "Effect": "Allow",
+ "Resource": "*"
+ },
+ {
+ "Action": "route53:GetChange",
+ "Effect": "Allow",
+ "Resource": "*"
+ },
+ {
+ "Action": "route53:changeResourceRecordSets",
+ "Condition": {
+ "ForAllValues:StringEquals": {
+ "route53:ChangeResourceRecordSetsActions": [
+ "UPSERT"
+ ],
+ "route53:ChangeResourceRecordSetsRecordTypes": [
+ "CNAME"
+ ]
+ },
+ "ForAllValues:StringLike": {
+ "route53:ChangeResourceRecordSetsNormalizedRecordNames": [
+ "*.dummy",
+ "*.dummy"
+ ]
+ }
+ },
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":route53:::hostedzone/DUMMY"
+ ]
+ ]
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorFunctionServiceRoleDefaultPolicy70B2B3EE",
+ "Roles": [
+ {
+ "Ref": "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorFunctionServiceRole53C01DAE"
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Policy"
+ },
+ "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorResource39D2FE55": {
+ "DeletionPolicy": "Delete",
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "DomainName": "dummy",
+ "HostedZoneId": "DUMMY",
+ "Region": "us-west-2",
+ "ServiceToken": {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructlistenercertificateCertificateRequestorFunction356EC2BA",
+ "Arn"
+ ]
+ },
+ "SubjectAlternativeNames": [
+ "*.dummy"
+ ],
+ "Tags": {
+ "Solutions:DeploymentUUID": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ },
+ "Type": "AWS::CloudFormation::CustomResource",
+ "UpdateReplacePolicy": "Delete"
+ },
+ "acdpbackstageloadbalancerconstructlistenerrule6E295068": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Actions": [
+ {
+ "TargetGroupArn": {
+ "Ref": "acdpbackstageloadbalancerconstructapplicationloadbalancerlistenerfleetGroupF9A486B0"
+ },
+ "Type": "forward"
+ }
+ ],
+ "Conditions": [
+ {
+ "Field": "path-pattern",
+ "PathPatternConfig": {
+ "Values": [
+ "*"
+ ]
+ }
+ }
+ ],
+ "ListenerArn": {
+ "Ref": "acdpbackstageloadbalancerconstructapplicationloadbalancerlistenerC3A53880"
+ },
+ "Priority": 1
+ },
+ "Type": "AWS::ElasticLoadBalancingV2::ListenerRule"
+ },
+ "acdpbackstageloadbalancerconstructrootarecordE1828906": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "AliasTarget": {
+ "DNSName": {
+ "Fn::Join": [
+ "",
+ [
+ "dualstack.",
+ {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructapplicationloadbalancer72C84123",
+ "DNSName"
+ ]
+ }
+ ]
+ ]
+ },
+ "HostedZoneId": {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructapplicationloadbalancer72C84123",
+ "CanonicalHostedZoneID"
+ ]
+ }
+ },
+ "HostedZoneId": "DUMMY",
+ "Name": "dummy.",
+ "Type": "A"
+ },
+ "Type": "AWS::Route53::RecordSet"
+ },
+ "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorFunction6186CB0F": {
+ "DependsOn": [
+ "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorFunctionServiceRoleDefaultPolicyBB84A042",
+ "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorFunctionServiceRoleEC4D3142",
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "Code": {
+ "S3Bucket": "cdk-hnb659fds-assets-test-account-id-us-west-2",
+ "S3Key": "test"
+ },
+ "Handler": "index.certificateRequestHandler",
+ "Role": {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorFunctionServiceRoleEC4D3142",
+ "Arn"
+ ]
+ },
+ "Runtime": "nodejs18.x",
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ],
+ "Timeout": 900
+ },
+ "Type": "AWS::Lambda::Function"
+ },
+ "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorFunctionServiceRoleDefaultPolicyBB84A042": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "acm:RequestCertificate",
+ "acm:DescribeCertificate",
+ "acm:DeleteCertificate",
+ "acm:AddTagsToCertificate"
+ ],
+ "Effect": "Allow",
+ "Resource": "*"
+ },
+ {
+ "Action": "route53:GetChange",
+ "Effect": "Allow",
+ "Resource": "*"
+ },
+ {
+ "Action": "route53:changeResourceRecordSets",
+ "Condition": {
+ "ForAllValues:StringEquals": {
+ "route53:ChangeResourceRecordSetsActions": [
+ "UPSERT"
+ ],
+ "route53:ChangeResourceRecordSetsRecordTypes": [
+ "CNAME"
+ ]
+ },
+ "ForAllValues:StringLike": {
+ "route53:ChangeResourceRecordSetsNormalizedRecordNames": [
+ "*.dummy",
+ "*.dummy"
+ ]
+ }
+ },
+ "Effect": "Allow",
+ "Resource": {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":route53:::hostedzone/DUMMY"
+ ]
+ ]
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "PolicyName": "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorFunctionServiceRoleDefaultPolicyBB84A042",
+ "Roles": [
+ {
+ "Ref": "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorFunctionServiceRoleEC4D3142"
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Policy"
+ },
+ "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorFunctionServiceRoleEC4D3142": {
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "AssumeRolePolicyDocument": {
+ "Statement": [
+ {
+ "Action": "sts:AssumeRole",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "lambda.amazonaws.com"
+ }
+ }
+ ],
+ "Version": "2012-10-17"
+ },
+ "ManagedPolicyArns": [
+ {
+ "Fn::Join": [
+ "",
+ [
+ "arn:",
+ {
+ "Ref": "AWS::Partition"
+ },
+ ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+ ]
+ ]
+ }
+ ],
+ "Tags": [
+ {
+ "Key": "Solutions:DeploymentUUID",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ ]
+ },
+ "Type": "AWS::IAM::Role"
+ },
+ "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorResource7E920D16": {
+ "DeletionPolicy": "Delete",
+ "DependsOn": [
+ "ssmappuniqueidregistermodule9C5C2C5D"
+ ],
+ "Properties": {
+ "DomainName": "dummy",
+ "HostedZoneId": "DUMMY",
+ "Region": "us-east-1",
+ "ServiceToken": {
+ "Fn::GetAtt": [
+ "acdpbackstageloadbalancerconstructuserpooldomaincertificateCertificateRequestorFunction6186CB0F",
+ "Arn"
+ ]
+ },
+ "SubjectAlternativeNames": [
+ "*.dummy"
+ ],
+ "Tags": {
+ "Solutions:DeploymentUUID": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/config/deployment-uuid}}"
+ ]
+ ]
+ }
+ }
+ },
+ "Type": "AWS::CloudFormation::CustomResource",
+ "UpdateReplacePolicy": "Delete"
+ },
+ "moduleinputsconstructssmacdpbuildssmprefixcreatessmparam81D8AF57": {
+ "Properties": {
+ "Name": {
+ "Fn::Join": [
+ "",
+ [
+ "/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/acdp-build/build-parameters"
+ ]
+ ]
+ },
+ "Type": "String",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/acdp-build"
+ ]
+ ]
+ }
+ },
+ "Type": "AWS::SSM::Parameter"
+ },
+ "ssmappuniqueidregistermodule9C5C2C5D": {
+ "Properties": {
+ "Description": "SSM parameter to register a module with an app unique ID.",
+ "Name": {
+ "Fn::Join": [
+ "",
+ [
+ "/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "/test-module-short-name"
+ ]
+ ]
+ },
+ "Type": "String",
+ "Value": {
+ "Fn::Join": [
+ "",
+ [
+ "{{resolve:ssm:/solution/",
+ {
+ "Ref": "AcdpUniqueId"
+ },
+ "}}"
+ ]
+ ]
+ }
+ },
+ "Type": "AWS::SSM::Parameter"
+ }
+ },
+ "Rules": {
+ "CheckBootstrapVersion": {
+ "Assertions": [
+ {
+ "Assert": {
+ "Fn::Not": [
+ {
+ "Fn::Contains": [
+ [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5"
+ ],
+ {
+ "Ref": "BootstrapVersion"
+ }
+ ]
+ }
+ ]
+ },
+ "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
+ }
+ ]
+ }
+ },
+ "Transform": "AWS::Serverless-2016-10-31"
+}
diff --git a/source/modules/acdp/backstage/cdk/source/tests/infrastructure/aspects/__init__.py b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/aspects/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/aspects/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/backstage/cdk/source/tests/infrastructure/aspects/test-cdk-nag-suppression-list.json b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/aspects/test-cdk-nag-suppression-list.json
similarity index 100%
rename from source/backstage/cdk/source/tests/infrastructure/aspects/test-cdk-nag-suppression-list.json
rename to source/modules/acdp/backstage/cdk/source/tests/infrastructure/aspects/test-cdk-nag-suppression-list.json
diff --git a/source/backstage/cdk/source/tests/infrastructure/aspects/test-cfn-nag-suppression-list.json b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/aspects/test-cfn-nag-suppression-list.json
similarity index 100%
rename from source/backstage/cdk/source/tests/infrastructure/aspects/test-cfn-nag-suppression-list.json
rename to source/modules/acdp/backstage/cdk/source/tests/infrastructure/aspects/test-cfn-nag-suppression-list.json
diff --git a/source/modules/acdp/backstage/cdk/source/tests/infrastructure/aspects/test_nag_suppression.py b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/aspects/test_nag_suppression.py
new file mode 100644
index 00000000..317133e2
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/aspects/test_nag_suppression.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+from os.path import dirname, realpath
+from typing import Any
+
+# AWS Libraries
+from aws_cdk import App, Stack, assertions, aws_kms
+from constructs import Construct
+
+# Connected Mobility Solution on AWS
+from ....infrastructure.aspects.backstage_nag_suppression import NagSuppression, NagType
+
+
+class NagTestStack(Stack):
+ def __init__(self, scope: Construct, construct_id: str, **kwargs: Any) -> None:
+ super().__init__(scope, construct_id, **kwargs)
+
+ self.test_key = aws_kms.Key(
+ self,
+ "nag-test-key",
+ enable_key_rotation=True,
+ )
+
+
+def test_nag_suppression_cdk_metadata() -> None:
+ app = App()
+ test_stack = NagTestStack(app, "nag-test-stack")
+ cdk_nag_suppression = NagSuppression(
+ f"{dirname(realpath(__file__))}/test-cdk-nag-suppression-list.json",
+ NagType.CDK_NAG,
+ )
+ l1_construct = test_stack.test_key.node.default_child
+ if l1_construct is not None:
+ cdk_nag_suppression.visit(l1_construct)
+ template = assertions.Template.from_stack(test_stack)
+ template.has_resource(
+ "AWS::KMS::Key",
+ {
+ "Metadata": {
+ "cdk_nag": {
+ "rules_to_suppress": [
+ {"id": "test-cdk-id", "reason": "test-cdk-reason"}
+ ]
+ }
+ }
+ },
+ )
+ else:
+ assert False
+
+
+def test_nag_suppression_cfn_metadata() -> None:
+ app = App()
+ test_stack = NagTestStack(app, "nag-test-stack")
+ cfn_nag_suppression = NagSuppression(
+ f"{dirname(realpath(__file__))}/test-cfn-nag-suppression-list.json",
+ NagType.CFN_NAG,
+ )
+
+ l1_construct = test_stack.test_key.node.default_child
+ if l1_construct is not None:
+ cfn_nag_suppression.visit(l1_construct)
+ template = assertions.Template.from_stack(test_stack)
+ template.has_resource(
+ "AWS::KMS::Key",
+ {
+ "Metadata": {
+ "cfn_nag": {
+ "rules_to_suppress": [
+ {"id": "test-cfn-id", "reason": "test-cfn-reason"}
+ ]
+ }
+ }
+ },
+ )
+ else:
+ assert False
diff --git a/source/modules/acdp/backstage/cdk/source/tests/infrastructure/fixtures/__init__.py b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/fixtures/__init__.py
new file mode 100644
index 00000000..d5980bc3
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/fixtures/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/backstage/cdk/source/tests/infrastructure/fixtures/fixture_stack_templates.py b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/fixtures/fixture_stack_templates.py
new file mode 100644
index 00000000..d41aa3d5
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/fixtures/fixture_stack_templates.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+# Standard Library
+import os
+
+# Third Party Libraries
+import pytest
+from syrupy.extensions.json import JSONSnapshotExtension
+from syrupy.matchers import path_value
+from syrupy.types import SerializableData
+
+# AWS Libraries
+import aws_cdk
+
+# Connected Mobility Solution on AWS
+from ....infrastructure.acdp_backstage_stack import AcdpBackstageStack
+from ....infrastructure.lib.cms_common.config.stack_inputs import (
+ S3AssetConfigInputs,
+ SolutionConfigInputs,
+)
+
+
+@pytest.fixture(name="snapshot_json_with_matcher")
+def fixture_snapshot_json_with_matcher(snapshot: SerializableData) -> SerializableData:
+ matcher = path_value(
+ mapping={
+ ".*": r"(\/?([0-9a-fA-F]+)\.zip|[a-zA-Z0-9:/-]+([0-9]{12})[a-zA-Z0-9:/-]+)",
+ },
+ regex=True,
+ types=(object,),
+ replacer=lambda data, match: data.replace(match[1], "test") if match else data,
+ )
+ return snapshot.use_extension(JSONSnapshotExtension)(matcher=matcher)
+
+
+@pytest.fixture(name="acdp_backstage_stack_template", scope="session")
+def fixture_acdp_backstage_stack_template() -> aws_cdk.assertions.Template:
+ os.environ["BACKSTAGE_IMAGE_TAG"] = "DUMMY"
+ os.environ["S3_ASSET_KEY_PREFIX"] = "asset-test.zip"
+ os.environ["USER_AGENT_STRING"] = "test-string"
+ os.environ["ROUTE53_HOSTED_ZONE_NAME"] = "dummy"
+ os.environ["ROUTE53_BASE_DOMAIN"] = "dummy"
+
+ solution_config_inputs = SolutionConfigInputs(
+ solution_id="test-solution-id",
+ solution_name="test-solution-name",
+ solution_version="test-solution-version",
+ application_type="test-application-type",
+ module_name="test-module-name",
+ module_short_name="test-module-short-name",
+ capability_id="test-capability-id",
+ )
+ s3_asset_config_inputs = S3AssetConfigInputs(
+ bucket_base_name="test-bucket-base-name",
+ object_key_prefix="test-object-key-prefix",
+ )
+
+ app = aws_cdk.App()
+ stack = AcdpBackstageStack(
+ app,
+ "backstage",
+ env=aws_cdk.Environment(
+ account="test-account-id",
+ region="us-west-2",
+ ),
+ solution_config_inputs=solution_config_inputs,
+ s3_asset_config_inputs=s3_asset_config_inputs,
+ s3_asset_bucket_name="test-bucket-name",
+ )
+ template = aws_cdk.assertions.Template.from_stack(stack)
+ return template
diff --git a/source/modules/acdp/backstage/cdk/source/tests/infrastructure/test_snapshot.py b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/test_snapshot.py
new file mode 100644
index 00000000..e567de9c
--- /dev/null
+++ b/source/modules/acdp/backstage/cdk/source/tests/infrastructure/test_snapshot.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Third Party Libraries
+from syrupy.types import SerializableData
+
+# AWS Libraries
+from aws_cdk.assertions import Template
+
+
+def test_acdp_backstage_snapshot(
+ acdp_backstage_stack_template: Template,
+ snapshot_json_with_matcher: SerializableData,
+) -> None:
+ assert acdp_backstage_stack_template.to_json() == snapshot_json_with_matcher
diff --git a/source/modules/acdp/backstage/docker-compose.yaml b/source/modules/acdp/backstage/docker-compose.yaml
new file mode 100644
index 00000000..43ca0ce4
--- /dev/null
+++ b/source/modules/acdp/backstage/docker-compose.yaml
@@ -0,0 +1,15 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+#This docker-compose deploys a postgres database that can be used for local testing in conjunction w/ app-config.local.yaml
+
+version: '3.8'
+services:
+ db:
+ image: postgres:14.1-alpine
+ restart: always
+ environment:
+ - POSTGRES_USER=test
+ - POSTGRES_PASSWORD=test
+ ports:
+ - '5432:5432'
diff --git a/source/modules/acdp/backstage/documentation/architecture/acdp-backstage-architecture-diagram.svg b/source/modules/acdp/backstage/documentation/architecture/acdp-backstage-architecture-diagram.svg
new file mode 100644
index 00000000..a534c6c6
--- /dev/null
+++ b/source/modules/acdp/backstage/documentation/architecture/acdp-backstage-architecture-diagram.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/documentation/sequence/cms-module-deployment-sequence-diagram.plantuml b/source/modules/acdp/backstage/documentation/sequence/cms-module-deployment-sequence-diagram.plantuml
similarity index 80%
rename from documentation/sequence/cms-module-deployment-sequence-diagram.plantuml
rename to source/modules/acdp/backstage/documentation/sequence/cms-module-deployment-sequence-diagram.plantuml
index 07d0b39c..b2a8d069 100644
--- a/documentation/sequence/cms-module-deployment-sequence-diagram.plantuml
+++ b/source/modules/acdp/backstage/documentation/sequence/cms-module-deployment-sequence-diagram.plantuml
@@ -28,33 +28,24 @@ actor User as user
box CMS Module Deployment via Backstage
participant "$ServerContentsIMG()\nBackstage Portal" as backstage << Backstage >>
participant "$SimpleStorageServiceIMG()\nCMS Assets Bucket" as s3 << S3 Asset >>
-participant "$ProtonIMG()\nProton" as proton << Proton >>
-participant "$CodeBuildIMG()\nProton" as pcb << CodeBuild >>
+participant "$CodeBuildIMG()\nACDP Build" as pcb << CodeBuild >>
participant "$CloudFormationIMG()\nCloudFormation" as cfn << CloudFormation >>
endbox
user -> backstage++ #777799: setup Backstage template component
backstage -> s3++ #3F8624: fetch template.yaml for module
return
-backstage -> proton++ #CC2264: link with Proton service template
-return
return create template component
-|||
user -> backstage++ #777799: deploy template component
-backstage -> s3++ #3F8624: write spec.yaml
-return
-|||
-backstage -> proton++ #CC2264: deploy service template
-proton -> pcb++ #3355DA: start CodeBuild steps
-pcb -> pcb: execute module deploy
-pcb -> cfn++ #CC2264: deploy module's infrastructure
-return
+backstage -> s3++ #3F8624: copy assets
return
-return
-|||
backstage -> s3++ #3F8624: write catalog-info.yaml
-backstage <-- s3:
+return
backstage -> backstage: register component
+backstage -> backstage: configure ACDP Deployment environment
+backstage -> pcb++ #CC2264: execute deploy build
+pcb -> cfn++ #CC2264: deploy module's infrastructure
+return
return
return
diff --git a/source/modules/acdp/backstage/documentation/sequence/cms-module-deployment-sequence-diagram.svg b/source/modules/acdp/backstage/documentation/sequence/cms-module-deployment-sequence-diagram.svg
new file mode 100644
index 00000000..aa8b763b
--- /dev/null
+++ b/source/modules/acdp/backstage/documentation/sequence/cms-module-deployment-sequence-diagram.svg
@@ -0,0 +1,197 @@
+
\ No newline at end of file
diff --git a/source/backstage/examples/entities.yaml b/source/modules/acdp/backstage/examples/entities.yaml
similarity index 100%
rename from source/backstage/examples/entities.yaml
rename to source/modules/acdp/backstage/examples/entities.yaml
diff --git a/source/backstage/examples/org.yaml b/source/modules/acdp/backstage/examples/org.yaml
similarity index 100%
rename from source/backstage/examples/org.yaml
rename to source/modules/acdp/backstage/examples/org.yaml
diff --git a/source/backstage/examples/template/content/catalog-info.yaml b/source/modules/acdp/backstage/examples/template/content/catalog-info.yaml
similarity index 100%
rename from source/backstage/examples/template/content/catalog-info.yaml
rename to source/modules/acdp/backstage/examples/template/content/catalog-info.yaml
diff --git a/source/backstage/examples/template/content/package.json b/source/modules/acdp/backstage/examples/template/content/package.json
similarity index 100%
rename from source/backstage/examples/template/content/package.json
rename to source/modules/acdp/backstage/examples/template/content/package.json
diff --git a/source/modules/acdp/backstage/examples/template/template.yaml b/source/modules/acdp/backstage/examples/template/template.yaml
new file mode 100644
index 00000000..944e5159
--- /dev/null
+++ b/source/modules/acdp/backstage/examples/template/template.yaml
@@ -0,0 +1,90 @@
+apiVersion: scaffolder.backstage.io/v1beta3
+kind: Template
+metadata:
+ description: A CDK Python app for showing a basic skeleton for a CMS module
+ name: cms-example-on-aws
+ tags:
+ - cms
+ - guide
+ - example
+ title: CMS Sample Module
+spec:
+ output:
+ links:
+ - entityRef: ${{ steps.catalogRegister.output.entityRef }}
+ icon: catalog
+ title: Open in catalog
+ owner: aws solutions
+ parameters:
+ - properties:
+ componentId:
+ default: cms-example-on-aws
+ description: Unique name of the component
+ pattern: '[a-zA-Z][-a-zA-Z0-9]*[a-zA-Z]'
+ title: Name
+ type: string
+ ui:field: EntityNamePicker
+ description:
+ default: A CDK Python app for showing a basic skeleton for a CMS module
+ description: Help others understand what this component is for.
+ title: Description
+ type: string
+ owner:
+ description: Owner of the component
+ title: Owner
+ type: string
+ ui:field: OwnerPicker
+ ui:options:
+ catalogFilter:
+ kind:
+ - Group
+ - User
+ required:
+ - componentId
+ - owner
+ title: Provide the required information
+ - properties:
+ appUniqueId:
+ default: cms
+ description: Application unique identifier used to uniquely name resources within the stack
+ title: App Unique ID
+ type: string
+ ui:disabled: true
+ required:
+ - appUniqueId
+ title: Provide the Module Configuration
+ steps:
+ - action: aws:acdp:deploy
+ id: acdpDeploy
+ input:
+ componentId: ${{ parameters.componentId }}
+ moduleParameters:
+ - name: APP_UNIQUE_ID
+ value: ${{ parameters.appUniqueId }}
+ name: ACDP Deploy
+ - action: aws:s3:catalog:write
+ id: s3CatalogWrite
+ input:
+ componentId: ${{ parameters.componentId }}
+ entity:
+ apiVersion: backstage.io/v1alpha1
+ kind: Component
+ metadata:
+ annotations:
+ aws.amazon.com/acdp-codebuild-project: ${{ steps.acdpDeploy.output.codeBuildProjectArn }}
+ backstage.io/techdocs-entity: component:default/cms-example-on-aws-docs
+ description: ${{parameters.description}}
+ labels:
+ templateName: cms-example-on-aws
+ name: ${{parameters.componentId}}
+ spec:
+ lifecycle: experimental
+ owner: ${{parameters.owner}}
+ type: service
+ name: S3 Catalog Write
+ - action: catalog:register
+ id: register
+ input:
+ catalogInfoUrl: ${{ steps.s3CatalogWrite.output.s3Url }}
+ name: Register
+ type: service
diff --git a/source/backstage/lerna.json b/source/modules/acdp/backstage/lerna.json
similarity index 100%
rename from source/backstage/lerna.json
rename to source/modules/acdp/backstage/lerna.json
diff --git a/source/modules/acdp/backstage/package.json b/source/modules/acdp/backstage/package.json
new file mode 100644
index 00000000..6b58a299
--- /dev/null
+++ b/source/modules/acdp/backstage/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "acdp-backstage",
+ "version": "1.1.0",
+ "private": true,
+ "license": "Apache-2.0",
+ "description": "Backstage implementation preconfigured to work with CMS",
+ "engines": {
+ "node": "18 || 20"
+ },
+ "scripts": {
+ "dev": "concurrently \"yarn start\" \"yarn start-backend\"",
+ "start": "yarn workspace app start",
+ "start-backend": "yarn workspace backend start",
+ "build:backend": "yarn workspace backend build",
+ "build:all": "backstage-cli repo build --all",
+ "build-image": "yarn workspace backend build-image",
+ "tsc": "tsc",
+ "tsc:full": "tsc --skipLibCheck false --incremental false",
+ "clean": "backstage-cli repo clean",
+ "test": "backstage-cli repo test --testTimeout 30000",
+ "test:all": "backstage-cli repo test --coverage --testTimeout 30000",
+ "lint": "backstage-cli repo lint --since origin/mainline",
+ "lint:all": "backstage-cli repo lint",
+ "prettier:check": "prettier --check .",
+ "new": "backstage-cli new --scope internal"
+ },
+ "workspaces": {
+ "packages": [
+ "packages/*",
+ "plugins/*"
+ ]
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.25.2",
+ "@types/supertest": "^2.0.14",
+ "concurrently": "^8.0.1",
+ "lerna": "^7.1.5",
+ "node-gyp": "^10.0.1",
+ "prettier": "^3",
+ "typescript": "^5.3.2",
+ "xml2js": "^0.5.0",
+ "yaml": "^2.2.2"
+ },
+ "resolutions": {
+ "@types/react": "^18",
+ "@types/react-dom": "^18"
+ },
+ "lint-staged": {
+ "*.{js,jsx,ts,tsx,mjs,cjs}": [
+ "eslint --fix",
+ "prettier --write"
+ ],
+ "*.{json,md}": [
+ "prettier --write"
+ ]
+ }
+}
diff --git a/source/backstage/packages/app/.eslintignore b/source/modules/acdp/backstage/packages/app/.eslintignore
similarity index 100%
rename from source/backstage/packages/app/.eslintignore
rename to source/modules/acdp/backstage/packages/app/.eslintignore
diff --git a/source/backstage/packages/app/.license-check.yaml b/source/modules/acdp/backstage/packages/app/.license-check.yaml
similarity index 100%
rename from source/backstage/packages/app/.license-check.yaml
rename to source/modules/acdp/backstage/packages/app/.license-check.yaml
diff --git a/source/backstage/packages/app/LICENSE b/source/modules/acdp/backstage/packages/app/LICENSE
similarity index 100%
rename from source/backstage/packages/app/LICENSE
rename to source/modules/acdp/backstage/packages/app/LICENSE
diff --git a/source/backstage/packages/app/cypress.json b/source/modules/acdp/backstage/packages/app/cypress.json
similarity index 100%
rename from source/backstage/packages/app/cypress.json
rename to source/modules/acdp/backstage/packages/app/cypress.json
diff --git a/source/backstage/packages/app/cypress/.eslintrc.json b/source/modules/acdp/backstage/packages/app/cypress/.eslintrc.json
similarity index 100%
rename from source/backstage/packages/app/cypress/.eslintrc.json
rename to source/modules/acdp/backstage/packages/app/cypress/.eslintrc.json
diff --git a/source/modules/acdp/backstage/packages/app/package.json b/source/modules/acdp/backstage/packages/app/package.json
new file mode 100644
index 00000000..4dd118e3
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/package.json
@@ -0,0 +1,96 @@
+{
+ "name": "app",
+ "version": "1.1.0",
+ "private": true,
+ "bundled": true,
+ "license": "Apache-2.0",
+ "description": "Backstage frontend package",
+ "backstage": {
+ "role": "frontend"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "clean": "backstage-cli package clean",
+ "test": "backstage-cli package test --coverage --silent",
+ "lint": "backstage-cli package lint",
+ "test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
+ "test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
+ "cy:dev": "cypress open",
+ "cy:run": "cypress run --browser chrome"
+ },
+ "dependencies": {
+ "@backstage/app-defaults": "^1.5.0",
+ "@backstage/catalog-model": "^1.4.4",
+ "@backstage/cli": "^0.25.2",
+ "@backstage/core-app-api": "^1.12.0",
+ "@backstage/core-components": "^0.14.0",
+ "@backstage/core-plugin-api": "^1.9.0",
+ "@backstage/integration-react": "^1.1.24",
+ "@backstage/plugin-api-docs": "^0.11.0",
+ "@backstage/plugin-catalog": "^1.17.0",
+ "@backstage/plugin-catalog-common": "^1.0.21",
+ "@backstage/plugin-catalog-graph": "^0.4.0",
+ "@backstage/plugin-catalog-import": "^0.10.6",
+ "@backstage/plugin-catalog-react": "^1.10.0",
+ "@backstage/plugin-home": "^0.6.2",
+ "@backstage/plugin-org": "^0.6.20",
+ "@backstage/plugin-permission-react": "^0.4.20",
+ "@backstage/plugin-scaffolder": "^1.18.0",
+ "@backstage/plugin-search": "^1.4.6",
+ "@backstage/plugin-search-react": "^1.7.6",
+ "@backstage/plugin-techdocs": "^1.10.0",
+ "@backstage/plugin-techdocs-module-addons-contrib": "^1.1.5",
+ "@backstage/plugin-techdocs-react": "^1.1.16",
+ "@backstage/plugin-user-settings": "^0.8.1",
+ "@backstage/theme": "^0.5.1",
+ "@react-hookz/web": "^23.1.0",
+ "backstage-plugin-acdp": "*",
+ "react": "^18.0.2",
+ "react-dom": "^18.0.2",
+ "react-router": "^6.3.0",
+ "react-router-dom": "^6.3.0",
+ "sanitize-html": "2.10.0"
+ },
+ "devDependencies": {
+ "@backstage/test-utils": "^1.5.0",
+ "@testing-library/dom": "^9.0.0",
+ "@testing-library/jest-dom": "^6.0.0",
+ "@testing-library/react": "^14.0.0",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/node": "20.1.1",
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "@types/react-router": "*",
+ "@types/react-router-dom": "*",
+ "@types/sanitize-html": "^2.9.0",
+ "cross-env": "7.0.3",
+ "cypress": "^13.3.0",
+ "eslint": "^8",
+ "eslint-plugin-cypress": "^2",
+ "jsonwebtoken": "9.0.0",
+ "start-server-and-test": "2.0.0"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "files": [
+ "dist"
+ ],
+ "jest": {
+ "coverageThreshold": {
+ "global": {
+ "lines": 80
+ }
+ }
+ }
+}
diff --git a/source/backstage/packages/app/public/android-chrome-192x192.png b/source/modules/acdp/backstage/packages/app/public/android-chrome-192x192.png
similarity index 100%
rename from source/backstage/packages/app/public/android-chrome-192x192.png
rename to source/modules/acdp/backstage/packages/app/public/android-chrome-192x192.png
diff --git a/source/backstage/packages/app/public/apple-touch-icon.png b/source/modules/acdp/backstage/packages/app/public/apple-touch-icon.png
similarity index 100%
rename from source/backstage/packages/app/public/apple-touch-icon.png
rename to source/modules/acdp/backstage/packages/app/public/apple-touch-icon.png
diff --git a/source/backstage/packages/app/public/favicon-16x16.png b/source/modules/acdp/backstage/packages/app/public/favicon-16x16.png
similarity index 100%
rename from source/backstage/packages/app/public/favicon-16x16.png
rename to source/modules/acdp/backstage/packages/app/public/favicon-16x16.png
diff --git a/source/backstage/packages/app/public/favicon-32x32.png b/source/modules/acdp/backstage/packages/app/public/favicon-32x32.png
similarity index 100%
rename from source/backstage/packages/app/public/favicon-32x32.png
rename to source/modules/acdp/backstage/packages/app/public/favicon-32x32.png
diff --git a/source/backstage/packages/app/public/favicon.ico b/source/modules/acdp/backstage/packages/app/public/favicon.ico
similarity index 100%
rename from source/backstage/packages/app/public/favicon.ico
rename to source/modules/acdp/backstage/packages/app/public/favicon.ico
diff --git a/source/backstage/packages/app/public/index.html b/source/modules/acdp/backstage/packages/app/public/index.html
similarity index 100%
rename from source/backstage/packages/app/public/index.html
rename to source/modules/acdp/backstage/packages/app/public/index.html
diff --git a/source/backstage/packages/app/public/manifest.json b/source/modules/acdp/backstage/packages/app/public/manifest.json
similarity index 100%
rename from source/backstage/packages/app/public/manifest.json
rename to source/modules/acdp/backstage/packages/app/public/manifest.json
diff --git a/source/backstage/packages/app/public/robots.txt b/source/modules/acdp/backstage/packages/app/public/robots.txt
similarity index 100%
rename from source/backstage/packages/app/public/robots.txt
rename to source/modules/acdp/backstage/packages/app/public/robots.txt
diff --git a/source/backstage/packages/app/public/safari-pinned-tab.svg b/source/modules/acdp/backstage/packages/app/public/safari-pinned-tab.svg
similarity index 100%
rename from source/backstage/packages/app/public/safari-pinned-tab.svg
rename to source/modules/acdp/backstage/packages/app/public/safari-pinned-tab.svg
diff --git a/source/modules/acdp/backstage/packages/app/src/App.tsx b/source/modules/acdp/backstage/packages/app/src/App.tsx
new file mode 100644
index 00000000..8ffed1c7
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/App.tsx
@@ -0,0 +1,149 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+import { Route } from "react-router-dom";
+import { apiDocsPlugin, ApiExplorerPage } from "@backstage/plugin-api-docs";
+import {
+ CatalogEntityPage,
+ CatalogIndexPage,
+ catalogPlugin,
+} from "@backstage/plugin-catalog";
+import {
+ CatalogImportPage,
+ catalogImportPlugin,
+} from "@backstage/plugin-catalog-import";
+import { ScaffolderPage, scaffolderPlugin } from "@backstage/plugin-scaffolder";
+import { orgPlugin } from "@backstage/plugin-org";
+import { SearchPage } from "@backstage/plugin-search";
+import {
+ TechDocsIndexPage,
+ techdocsPlugin,
+ TechDocsReaderPage,
+} from "@backstage/plugin-techdocs";
+import { TechDocsAddons } from "@backstage/plugin-techdocs-react";
+import { ReportIssue } from "@backstage/plugin-techdocs-module-addons-contrib";
+import { UserSettingsPage } from "@backstage/plugin-user-settings";
+import { HomepageCompositionRoot } from "@backstage/plugin-home";
+import { apis } from "./apis";
+import { entityPage } from "./components/catalog/EntityPage";
+import { searchPage } from "./components/search/SearchPage";
+import { Root } from "./components/Root";
+
+import {
+ AlertDisplay,
+ OAuthRequestDialog,
+ SignInPage,
+} from "@backstage/core-components";
+import { createApp } from "@backstage/app-defaults";
+import { AppRouter, FlatRoutes } from "@backstage/core-app-api";
+import { CatalogGraphPage } from "@backstage/plugin-catalog-graph";
+import { RequirePermission } from "@backstage/plugin-permission-react";
+import { catalogEntityCreatePermission } from "@backstage/plugin-catalog-common/alpha";
+
+import { cognitoAuthApiRef } from "./custom/AwsCognitoAuth";
+import {
+ discoveryApiRef,
+ useApi,
+ IdentityApi,
+ configApiRef,
+} from "@backstage/core-plugin-api";
+import { setTokenCookie } from "./custom/CookieAuth";
+
+import { HomePage } from "./components/home/HomePage";
+
+const app = createApp({
+ apis,
+ components: {
+ SignInPage: (props) => {
+ const configApi = useApi(configApiRef);
+ const discoveryApi = useApi(discoveryApiRef);
+ if (configApi.getString("auth.environment") === "development") {
+ return ;
+ }
+ return (
+ {
+ setTokenCookie(
+ await discoveryApi.getBaseUrl("cookie"),
+ identityApi,
+ );
+ props.onSignInSuccess(identityApi);
+ }}
+ />
+ );
+ },
+ },
+ bindRoutes({ bind }) {
+ bind(catalogPlugin.externalRoutes, {
+ createComponent: scaffolderPlugin.routes.root,
+ viewTechDoc: techdocsPlugin.routes.docRoot,
+ });
+ bind(apiDocsPlugin.externalRoutes, {
+ registerApi: catalogImportPlugin.routes.importPage,
+ });
+ bind(scaffolderPlugin.externalRoutes, {
+ registerComponent: catalogImportPlugin.routes.importPage,
+ });
+ bind(orgPlugin.externalRoutes, {
+ catalogIndex: catalogPlugin.routes.catalogIndex,
+ });
+ },
+});
+
+const routes = (
+
+ }>
+
+
+ } />
+ }
+ >
+ {entityPage}
+
+ } />
+ }
+ >
+
+
+
+
+ } />
+ } />
+
+
+
+ }
+ />
+ }>
+ {searchPage}
+
+ } />
+ } />
+
+);
+
+export default app.createRoot(
+ <>
+
+
+
+ {routes}
+
+ >,
+);
diff --git a/source/modules/acdp/backstage/packages/app/src/__tests__/App.test.tsx b/source/modules/acdp/backstage/packages/app/src/__tests__/App.test.tsx
new file mode 100644
index 00000000..a3495eb1
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/__tests__/App.test.tsx
@@ -0,0 +1,36 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+import { renderWithEffects } from "@backstage/test-utils";
+import App from "../App";
+
+beforeAll(() => {
+ window.open = jest.fn();
+});
+
+describe("App", () => {
+ it("should render", async () => {
+ process.env = {
+ NODE_ENV: "test",
+ APP_CONFIG: [
+ {
+ data: {
+ app: { title: "Test" },
+ auth: {
+ environment: "production",
+ },
+ backend: { baseUrl: "http://localhost:7007" },
+ techdocs: {
+ storageUrl: "http://localhost:7007/api/techdocs/static/docs",
+ },
+ },
+ context: "test",
+ },
+ ] as any,
+ };
+
+ const rendered = await renderWithEffects();
+ expect(rendered.baseElement).toBeInTheDocument();
+ });
+});
diff --git a/source/modules/acdp/backstage/packages/app/src/apis.ts b/source/modules/acdp/backstage/packages/app/src/apis.ts
new file mode 100644
index 00000000..bdbfcbcc
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/apis.ts
@@ -0,0 +1,47 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ ScmIntegrationsApi,
+ scmIntegrationsApiRef,
+ ScmAuth,
+} from "@backstage/integration-react";
+import {
+ AnyApiFactory,
+ configApiRef,
+ createApiFactory,
+ discoveryApiRef,
+ oauthRequestApiRef,
+} from "@backstage/core-plugin-api";
+
+import { cognitoAuthApiRef } from "./custom/AwsCognitoAuth";
+import { OAuth2 } from "@backstage/core-app-api";
+import { UserIcon } from "@backstage/core-components";
+
+export const apis: AnyApiFactory[] = [
+ createApiFactory({
+ api: cognitoAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ OAuth2.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getOptionalString("auth.environment"),
+ provider: {
+ id: "cognito",
+ title: "AWS Cognito",
+ icon: UserIcon,
+ },
+ }),
+ }),
+ createApiFactory({
+ api: scmIntegrationsApiRef,
+ deps: { configApi: configApiRef },
+ factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
+ }),
+ ScmAuth.createDefaultApiFactory(),
+];
diff --git a/source/backstage/packages/app/src/components/Root/LogoFull.tsx b/source/modules/acdp/backstage/packages/app/src/components/Root/LogoFull.tsx
similarity index 99%
rename from source/backstage/packages/app/src/components/Root/LogoFull.tsx
rename to source/modules/acdp/backstage/packages/app/src/components/Root/LogoFull.tsx
index a644ae72..37671bb5 100644
--- a/source/backstage/packages/app/src/components/Root/LogoFull.tsx
+++ b/source/modules/acdp/backstage/packages/app/src/components/Root/LogoFull.tsx
@@ -1,16 +1,16 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import React from 'react';
-import { makeStyles } from '@material-ui/core';
+import React from "react";
+import { makeStyles } from "@material-ui/core";
const useStyles = makeStyles({
svg: {
- width: 'auto',
+ width: "auto",
height: 30,
},
path: {
- fill: '#7df3e1',
+ fill: "#7df3e1",
},
});
const LogoFull = () => {
diff --git a/source/backstage/packages/app/src/components/Root/LogoIcon.tsx b/source/modules/acdp/backstage/packages/app/src/components/Root/LogoIcon.tsx
similarity index 96%
rename from source/backstage/packages/app/src/components/Root/LogoIcon.tsx
rename to source/modules/acdp/backstage/packages/app/src/components/Root/LogoIcon.tsx
index 927ae8b1..ce775e07 100644
--- a/source/backstage/packages/app/src/components/Root/LogoIcon.tsx
+++ b/source/modules/acdp/backstage/packages/app/src/components/Root/LogoIcon.tsx
@@ -1,16 +1,16 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import React from 'react';
-import { makeStyles } from '@material-ui/core';
+import React from "react";
+import { makeStyles } from "@material-ui/core";
const useStyles = makeStyles({
svg: {
- width: 'auto',
+ width: "auto",
height: 28,
},
path: {
- fill: '#7df3e1',
+ fill: "#7df3e1",
},
});
diff --git a/source/modules/acdp/backstage/packages/app/src/components/Root/Root.tsx b/source/modules/acdp/backstage/packages/app/src/components/Root/Root.tsx
new file mode 100644
index 00000000..2e4a580b
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/components/Root/Root.tsx
@@ -0,0 +1,90 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React, { PropsWithChildren } from "react";
+import { makeStyles } from "@material-ui/core";
+import HomeIcon from "@material-ui/icons/Home";
+import ExtensionIcon from "@material-ui/icons/Extension";
+import CategoryIcon from "@material-ui/icons/Category";
+import LibraryBooks from "@material-ui/icons/LibraryBooks";
+import CreateComponentIcon from "@material-ui/icons/AddCircleOutline";
+import LogoFull from "./LogoFull";
+import LogoIcon from "./LogoIcon";
+import {
+ Settings as SidebarSettings,
+ UserSettingsSignInAvatar,
+} from "@backstage/plugin-user-settings";
+import { SidebarSearchModal } from "@backstage/plugin-search";
+import {
+ Sidebar,
+ sidebarConfig,
+ SidebarDivider,
+ SidebarGroup,
+ SidebarItem,
+ SidebarPage,
+ SidebarSpace,
+ useSidebarOpenState,
+ Link,
+} from "@backstage/core-components";
+import MenuIcon from "@material-ui/icons/Menu";
+import SearchIcon from "@material-ui/icons/Search";
+
+const useSidebarLogoStyles = makeStyles({
+ root: {
+ width: sidebarConfig.drawerWidthClosed,
+ height: 3 * sidebarConfig.logoHeight,
+ display: "flex",
+ flexFlow: "row nowrap",
+ alignItems: "center",
+ marginBottom: -14,
+ },
+ link: {
+ width: sidebarConfig.drawerWidthClosed,
+ marginLeft: 24,
+ },
+});
+
+const SidebarLogo = () => {
+ const classes = useSidebarLogoStyles();
+ const { isOpen } = useSidebarOpenState();
+
+ return (
+
+
+ {isOpen ? : }
+
+
+ );
+};
+
+export const Root = ({ children }: PropsWithChildren<{}>) => (
+
+
+
+ } to="/search">
+
+
+
+ }>
+ {/* Global nav, not org-specific */}
+
+
+
+
+
+ {/* End global nav */}
+
+
+
+
+ }
+ to="/settings"
+ >
+
+
+
+ {children}
+
+);
diff --git a/source/modules/acdp/backstage/packages/app/src/components/Root/index.ts b/source/modules/acdp/backstage/packages/app/src/components/Root/index.ts
new file mode 100644
index 00000000..74a9b2fe
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/components/Root/index.ts
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export { Root } from "./Root";
diff --git a/source/modules/acdp/backstage/packages/app/src/components/catalog/EntityConditions.tsx b/source/modules/acdp/backstage/packages/app/src/components/catalog/EntityConditions.tsx
new file mode 100644
index 00000000..b17e9f79
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/components/catalog/EntityConditions.tsx
@@ -0,0 +1,38 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Entity } from "@backstage/catalog-model";
+
+import { constants } from "backstage-plugin-acdp-common";
+
+export function hasDocs(): (entity: Entity) => boolean {
+ return (entity: Entity) => {
+ return Boolean(
+ entity.metadata.annotations?.[constants.BACKSTAGE_TECHDOCS_ANNOTATION],
+ );
+ };
+}
+
+export function hasCicd(): (entity: Entity) => boolean {
+ return (entity: Entity) => {
+ return Boolean(
+ entity.metadata.annotations?.[
+ constants.ACDP_DEPLOYMENT_TARGET_ANNOTATION
+ ],
+ );
+ };
+}
+
+export function hasApis(): (entity: Entity) => boolean {
+ return (entity: Entity) => {
+ return (
+ Boolean(entity.spec?.providesApis) || Boolean(entity.spec?.consumesApis)
+ );
+ };
+}
+
+export function hasDependencies(): (entity: Entity) => boolean {
+ return (entity: Entity) => {
+ return Boolean(entity.spec?.dependsOn);
+ };
+}
diff --git a/source/modules/acdp/backstage/packages/app/src/components/catalog/EntityContent.tsx b/source/modules/acdp/backstage/packages/app/src/components/catalog/EntityContent.tsx
new file mode 100644
index 00000000..6bed4329
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/components/catalog/EntityContent.tsx
@@ -0,0 +1,102 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+import { Grid } from "@material-ui/core";
+import {
+ EntityConsumedApisCard,
+ EntityProvidedApisCard,
+} from "@backstage/plugin-api-docs";
+import {
+ EntityAboutCard,
+ EntityDependsOnComponentsCard,
+ EntityDependsOnResourcesCard,
+ EntityHasSubcomponentsCard,
+ EntityLinksCard,
+ EntitySwitch,
+ EntityOrphanWarning,
+ EntityProcessingErrorsPanel,
+ hasCatalogProcessingErrors,
+ isOrphan,
+} from "@backstage/plugin-catalog";
+import { EntityTechdocsContent } from "@backstage/plugin-techdocs";
+import { EntityCatalogGraphCard } from "@backstage/plugin-catalog-graph";
+
+import { TechDocsAddons } from "@backstage/plugin-techdocs-react";
+import { ReportIssue } from "@backstage/plugin-techdocs-module-addons-contrib";
+
+import { EntityAcdpBuildProjectOverviewCard } from "backstage-plugin-acdp";
+
+export const entityWarningContent = (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+export const overviewContent = (
+
+ {entityWarningContent}
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export const cicdContent = (
+
+
+
+);
+
+export const apiContent = (
+
+
+
+
+
+
+
+
+);
+
+export const dependenciesContent = (
+
+
+
+
+
+
+
+
+);
+
+export const techdocsContent = (
+
+
+
+
+
+);
diff --git a/source/modules/acdp/backstage/packages/app/src/components/catalog/EntityPage.tsx b/source/modules/acdp/backstage/packages/app/src/components/catalog/EntityPage.tsx
new file mode 100644
index 00000000..815cb389
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/components/catalog/EntityPage.tsx
@@ -0,0 +1,243 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+import { Grid } from "@material-ui/core";
+import {
+ EntityApiDefinitionCard,
+ EntityConsumingComponentsCard,
+ EntityHasApisCard,
+ EntityProvidingComponentsCard,
+} from "@backstage/plugin-api-docs";
+import {
+ EntityAboutCard,
+ EntityHasComponentsCard,
+ EntityHasResourcesCard,
+ EntityHasSystemsCard,
+ EntityLayout,
+ EntityLinksCard,
+ EntitySwitch,
+ isKind,
+} from "@backstage/plugin-catalog";
+import {
+ EntityUserProfileCard,
+ EntityGroupProfileCard,
+ EntityMembersListCard,
+ EntityOwnershipCard,
+} from "@backstage/plugin-org";
+import {
+ Direction,
+ EntityCatalogGraphCard,
+} from "@backstage/plugin-catalog-graph";
+import {
+ RELATION_API_CONSUMED_BY,
+ RELATION_API_PROVIDED_BY,
+ RELATION_CONSUMES_API,
+ RELATION_DEPENDENCY_OF,
+ RELATION_DEPENDS_ON,
+ RELATION_HAS_PART,
+ RELATION_PART_OF,
+ RELATION_PROVIDES_API,
+} from "@backstage/catalog-model";
+
+import {
+ entityWarningContent,
+ overviewContent,
+ cicdContent,
+ dependenciesContent,
+ techdocsContent,
+ apiContent,
+} from "./EntityContent";
+import { hasDocs, hasCicd, hasApis, hasDependencies } from "./EntityConditions";
+
+const componentPage = (
+
+
+ {overviewContent}
+
+
+
+ {cicdContent}
+
+
+
+ {apiContent}
+
+
+
+ {dependenciesContent}
+
+
+
+ {techdocsContent}
+
+
+);
+
+/**
+ * NOTE: This page is designed to work on small screens such as mobile devices.
+ * This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`,
+ * since this does not default. If no breakpoints are used, the items will equitably share the available space.
+ * https://material-ui.com/components/grid/#basic-grid.
+ */
+
+const defaultEntityPage = (
+
+
+ {overviewContent}
+
+
+);
+
+const apiPage = (
+
+
+
+ {entityWarningContent}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+const userPage = (
+
+
+
+ {entityWarningContent}
+
+
+
+
+
+
+
+
+
+);
+
+const groupPage = (
+
+
+
+ {entityWarningContent}
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+const systemPage = (
+
+
+
+ {entityWarningContent}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+const domainPage = (
+
+
+
+ {entityWarningContent}
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export const entityPage = (
+
+
+
+
+
+
+
+
+ {defaultEntityPage}
+
+);
diff --git a/source/modules/acdp/backstage/packages/app/src/components/home/HomePage.tsx b/source/modules/acdp/backstage/packages/app/src/components/home/HomePage.tsx
new file mode 100644
index 00000000..aa2fc78e
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/components/home/HomePage.tsx
@@ -0,0 +1,84 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Page, Header, Content } from "@backstage/core-components";
+import {
+ ClockConfig,
+ HeaderWorldClock,
+ HomePageStarredEntities,
+} from "@backstage/plugin-home";
+import { HomePageSearchBar } from "@backstage/plugin-search";
+import { Grid, makeStyles } from "@material-ui/core";
+import { useUserProfile } from "@backstage/plugin-user-settings";
+import React from "react";
+
+export const HomePage = () => {
+ const clockConfigs: ClockConfig[] = [
+ {
+ label: "East Coast",
+ timeZone: "America/New_York",
+ },
+ {
+ label: "Central",
+ timeZone: "America/Chicago",
+ },
+ {
+ label: "Mountain",
+ timeZone: "America/Denver",
+ },
+ {
+ label: "Pacific",
+ timeZone: "America/Los_Angeles",
+ },
+ ];
+
+ const timeFormat: Intl.DateTimeFormatOptions = {
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: true,
+ };
+
+ const userProfile = useUserProfile();
+
+ const useStyles = makeStyles((theme) => ({
+ searchBar: {
+ display: "flex",
+ maxWidth: "60vw",
+ backgroundColor: theme.palette.background.paper,
+ boxShadow: theme.shadows[1],
+ borderRadius: "50px",
+ margin: "auto",
+ },
+ }));
+
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/source/modules/acdp/backstage/packages/app/src/components/search/SearchPage.tsx b/source/modules/acdp/backstage/packages/app/src/components/search/SearchPage.tsx
new file mode 100644
index 00000000..401b98e2
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/components/search/SearchPage.tsx
@@ -0,0 +1,127 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+import { makeStyles, Theme, Grid, Paper } from "@material-ui/core";
+
+import { CatalogSearchResultListItem } from "@backstage/plugin-catalog";
+import {
+ catalogApiRef,
+ CATALOG_FILTER_EXISTS,
+} from "@backstage/plugin-catalog-react";
+import { TechDocsSearchResultListItem } from "@backstage/plugin-techdocs";
+
+import { SearchType } from "@backstage/plugin-search";
+import {
+ SearchBar,
+ SearchFilter,
+ SearchResult,
+ SearchPagination,
+ useSearch,
+} from "@backstage/plugin-search-react";
+import {
+ CatalogIcon,
+ Content,
+ DocsIcon,
+ Header,
+ Page,
+} from "@backstage/core-components";
+import { useApi } from "@backstage/core-plugin-api";
+
+const useStyles = makeStyles((theme: Theme) => ({
+ bar: {
+ padding: theme.spacing(1, 0),
+ },
+ filters: {
+ padding: theme.spacing(2),
+ marginTop: theme.spacing(2),
+ },
+ filter: {
+ "& + &": {
+ marginTop: theme.spacing(2.5),
+ },
+ },
+}));
+
+const SearchPage = () => {
+ const classes = useStyles();
+ const { types } = useSearch();
+ const catalogApi = useApi(catalogApiRef);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ ,
+ },
+ {
+ value: "techdocs",
+ name: "Documentation",
+ icon: ,
+ },
+ ]}
+ />
+
+ {types.includes("techdocs") && (
+ {
+ // Return a list of entities which are documented.
+ const { items } = await catalogApi.getEntities({
+ fields: ["metadata.name"],
+ filter: {
+ "metadata.annotations.backstage.io/techdocs-ref":
+ CATALOG_FILTER_EXISTS,
+ },
+ });
+
+ const names = items.map((entity) => entity.metadata.name);
+ names.sort();
+ return names;
+ }}
+ />
+ )}
+
+
+
+
+
+
+
+ } />
+ } />
+
+
+
+
+
+ );
+};
+
+export const searchPage = ;
diff --git a/source/backstage/packages/app/src/custom/AwsCognitoAuth.ts b/source/modules/acdp/backstage/packages/app/src/custom/AwsCognitoAuth.ts
similarity index 86%
rename from source/backstage/packages/app/src/custom/AwsCognitoAuth.ts
rename to source/modules/acdp/backstage/packages/app/src/custom/AwsCognitoAuth.ts
index 74b15207..5cd18705 100644
--- a/source/backstage/packages/app/src/custom/AwsCognitoAuth.ts
+++ b/source/modules/acdp/backstage/packages/app/src/custom/AwsCognitoAuth.ts
@@ -9,7 +9,7 @@ import {
OpenIdConnectApi,
ProfileInfoApi,
SessionApi,
-} from '@backstage/core-plugin-api';
+} from "@backstage/core-plugin-api";
export const cognitoAuthApiRef: ApiRef<
OAuthApi &
@@ -18,5 +18,5 @@ export const cognitoAuthApiRef: ApiRef<
BackstageIdentityApi &
SessionApi
> = createApiRef({
- id: 'core.auth.cognito',
+ id: "core.auth.cognito",
});
diff --git a/source/backstage/packages/app/src/custom/CookieAuth.ts b/source/modules/acdp/backstage/packages/app/src/custom/CookieAuth.ts
similarity index 77%
rename from source/backstage/packages/app/src/custom/CookieAuth.ts
rename to source/modules/acdp/backstage/packages/app/src/custom/CookieAuth.ts
index 7d8db272..f093dbb4 100644
--- a/source/backstage/packages/app/src/custom/CookieAuth.ts
+++ b/source/modules/acdp/backstage/packages/app/src/custom/CookieAuth.ts
@@ -1,22 +1,22 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import type { IdentityApi } from '@backstage/core-plugin-api';
+import type { IdentityApi } from "@backstage/core-plugin-api";
// Parses supplied JWT token and returns the payload
function parseJwt(token: string): { exp: number } {
- const base64Url = token.split('.')[1];
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+ const base64Url = token.split(".")[1];
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const jsonPayload = decodeURIComponent(
- Buffer.from(base64, 'base64')
+ Buffer.from(base64, "base64")
.toString()
- .split('')
+ .split("")
.map(
- c =>
+ (c) =>
// eslint-disable-next-line prefer-template
- '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2),
+ "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2),
)
- .join(''),
+ .join(""),
);
return JSON.parse(jsonPayload);
@@ -39,8 +39,8 @@ export async function setTokenCookie(url: string, identityApi: IdentityApi) {
}
await fetch(url, {
- mode: 'cors',
- credentials: 'include',
+ mode: "cors",
+ credentials: "include",
headers: {
Authorization: `Bearer ${token}`,
},
diff --git a/source/modules/acdp/backstage/packages/app/src/custom/__tests__/CookieAuth.test.ts b/source/modules/acdp/backstage/packages/app/src/custom/__tests__/CookieAuth.test.ts
new file mode 100644
index 00000000..174e08eb
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/custom/__tests__/CookieAuth.test.ts
@@ -0,0 +1,44 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { setTokenCookie } from "../CookieAuth";
+import type { IdentityApi } from "@backstage/core-plugin-api";
+import jwt from "jsonwebtoken";
+
+beforeAll(() => {
+ global.fetch = jest.fn();
+ jest.useFakeTimers();
+});
+
+afterEach(() => {
+ jest.resetAllMocks();
+});
+
+describe("CookieAuth", () => {
+ it("Should call endpoint to set token cookie", async () => {
+ const mockJwt = jwt.sign({ test: "test" }, "test", { expiresIn: "1h" });
+ const mockIdentityApi: IdentityApi = {
+ getBackstageIdentity: jest.fn(),
+ getCredentials: jest.fn().mockReturnValue({ token: mockJwt }),
+ getProfileInfo: jest.fn(),
+ signOut: jest.fn(),
+ };
+
+ await setTokenCookie("https://localhost:3000", mockIdentityApi);
+ expect(mockIdentityApi.getCredentials).toBeCalledTimes(1);
+ jest.runOnlyPendingTimers();
+ expect(mockIdentityApi.getCredentials).toBeCalledTimes(2);
+ });
+
+ it("Should not call endpoint to set token cookie if token is null", async () => {
+ const mockIdentityApi: IdentityApi = {
+ getBackstageIdentity: jest.fn(),
+ getCredentials: jest.fn().mockReturnValue({ token: null }),
+ getProfileInfo: jest.fn(),
+ signOut: jest.fn(),
+ };
+ await setTokenCookie("https://localhost:3000", mockIdentityApi);
+ expect(mockIdentityApi.getCredentials).toBeCalled();
+ expect(global.fetch).not.toBeCalled();
+ });
+});
diff --git a/source/modules/acdp/backstage/packages/app/src/index.tsx b/source/modules/acdp/backstage/packages/app/src/index.tsx
new file mode 100644
index 00000000..314bcb27
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/index.tsx
@@ -0,0 +1,9 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import "@backstage/cli/asset-types";
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App";
+
+ReactDOM.render(, document.getElementById("root"));
diff --git a/source/modules/acdp/backstage/packages/app/src/setupTests.ts b/source/modules/acdp/backstage/packages/app/src/setupTests.ts
new file mode 100644
index 00000000..7ea7f359
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/app/src/setupTests.ts
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import "@testing-library/jest-dom";
diff --git a/source/backstage/packages/backend/.license-check.yaml b/source/modules/acdp/backstage/packages/backend/.license-check.yaml
similarity index 100%
rename from source/backstage/packages/backend/.license-check.yaml
rename to source/modules/acdp/backstage/packages/backend/.license-check.yaml
diff --git a/source/backstage/packages/backend/Dockerfile b/source/modules/acdp/backstage/packages/backend/Dockerfile
similarity index 97%
rename from source/backstage/packages/backend/Dockerfile
rename to source/modules/acdp/backstage/packages/backend/Dockerfile
index 589e05d4..bc871a05 100644
--- a/source/backstage/packages/backend/Dockerfile
+++ b/source/modules/acdp/backstage/packages/backend/Dockerfile
@@ -53,4 +53,4 @@ RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid
COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
-CMD ["node", "packages/backend", "--config", "app-config.yaml"]
+CMD ["node", "packages/backend", "--config", "app-config.production.yaml"]
diff --git a/source/backstage/packages/backend/LICENSE b/source/modules/acdp/backstage/packages/backend/LICENSE
similarity index 100%
rename from source/backstage/packages/backend/LICENSE
rename to source/modules/acdp/backstage/packages/backend/LICENSE
diff --git a/source/modules/acdp/backstage/packages/backend/package.json b/source/modules/acdp/backstage/packages/backend/package.json
new file mode 100644
index 00000000..42f38de7
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "backend",
+ "version": "1.1.0",
+ "main": "dist/index.cjs.js",
+ "types": "src/index.ts",
+ "private": true,
+ "license": "Apache-2.0",
+ "description": "Backstage backend package",
+ "backstage": {
+ "role": "backend"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test --coverage --silent",
+ "clean": "backstage-cli package clean",
+ "build-image": "docker build ../.. -f Dockerfile --tag backstage"
+ },
+ "dependencies": {
+ "@aws-sdk/client-cognito-identity-provider": "3.515.0",
+ "@backstage/backend-common": "^0.21.3",
+ "@backstage/backend-tasks": "^0.5.18",
+ "@backstage/catalog-client": "^1.6.0",
+ "@backstage/catalog-model": "^1.4.4",
+ "@backstage/config": "^1.1.1",
+ "@backstage/plugin-app-backend": "^0.3.61",
+ "@backstage/plugin-auth-backend": "^0.21.3",
+ "@backstage/plugin-auth-node": "^0.4.8",
+ "@backstage/plugin-catalog-backend": "^1.17.3",
+ "@backstage/plugin-catalog-backend-module-aws": "^0.3.7",
+ "@backstage/plugin-events-backend": "^0.2.22",
+ "@backstage/plugin-permission-common": "^0.7.12",
+ "@backstage/plugin-permission-node": "^0.7.24",
+ "@backstage/plugin-proxy-backend": "^0.4.11",
+ "@backstage/plugin-scaffolder-backend": "^1.21.3",
+ "@backstage/plugin-search-backend": "^1.5.3",
+ "@backstage/plugin-search-backend-module-pg": "^0.5.22",
+ "@backstage/plugin-search-backend-node": "^1.2.17",
+ "@backstage/plugin-techdocs-backend": "^1.9.6",
+ "app": "file:../app",
+ "backstage-plugin-acdp-backend": "*",
+ "jwt-decode": "^3.1.0",
+ "prettier": "^3"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.25.2",
+ "@types/cookie-parser": "1.4.3",
+ "@types/dockerode": "3.3.17",
+ "@types/lodash": "^4.17.0",
+ "@types/luxon": "3.3.0",
+ "@types/passport-oauth2": "1.4.12",
+ "@types/uuid": "^9.0.2",
+ "supertest": "^6.3.3"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/source/modules/acdp/backstage/packages/backend/src/alb-auth/middleware.ts b/source/modules/acdp/backstage/packages/backend/src/alb-auth/middleware.ts
new file mode 100644
index 00000000..ad90e443
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/alb-auth/middleware.ts
@@ -0,0 +1,97 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import type { Config } from "@backstage/config";
+import { getBearerTokenFromAuthorizationHeader } from "@backstage/plugin-auth-node";
+import { NextFunction, Request, Response, RequestHandler } from "express";
+import { PluginEnvironment } from "../types";
+import { AuthenticationError } from "@backstage/errors";
+import { decodeJwt } from "jose";
+
+function setTokenCookie(
+ res: Response,
+ options: { token: string; secure: boolean; cookieDomain: string },
+) {
+ try {
+ const payload = decodeJwt(options.token);
+ res.cookie("token", options.token, {
+ expires: new Date(payload.exp ? payload.exp * 1000 : 0),
+ secure: options.secure,
+ sameSite: "lax",
+ domain: options.cookieDomain,
+ path: "/",
+ httpOnly: true,
+ });
+ } catch (_err) {
+ // Ignore
+ }
+}
+
+export const createAuthMiddleware = async (
+ config: Config,
+ appEnv: PluginEnvironment,
+) => {
+ const authMiddleware: RequestHandler = async (
+ req: Request,
+ res: Response,
+ next: NextFunction,
+ ) => {
+ try {
+ appEnv.logger.debug(`ALB Headers [${JSON.stringify(req.headers)}]`);
+ const token =
+ getBearerTokenFromAuthorizationHeader(req.headers.authorization) ||
+ (req.cookies?.token as string | undefined) ||
+ (req.headers["x-amzn-oidc-data"] as string | undefined);
+
+ if (!token) {
+ res.status(401).send("Unauthorized");
+ return;
+ }
+ if (!req.headers.authorization) {
+ // getIdentity only seems to work off this header, coalesce all token options to this
+ req.headers.authorization = `Bearer ${token}`;
+ }
+
+ try {
+ //detect backend service generated call and approve
+ await appEnv.tokenManager.authenticate(token);
+ appEnv.logger.debug(`Successfully authenticated as service user`);
+ next();
+ return;
+ } catch (error) {
+ if (error instanceof AuthenticationError) {
+ appEnv.logger.debug(`Token is not a valid service token`);
+ } else {
+ //not an expected error for token failure
+ throw error;
+ }
+ }
+
+ req.user = await appEnv.identity.getIdentity({ request: req });
+
+ if (!req.user) {
+ throw new Error("getIdentity failed to set user");
+ }
+
+ appEnv.logger.debug(`Successfully authenticated as user`);
+
+ if (token && token !== req.cookies?.token) {
+ const baseUrl = config.getString("backend.baseUrl");
+ const secure = baseUrl.startsWith("https://");
+ const cookieDomain = new URL(baseUrl).hostname;
+
+ setTokenCookie(res, {
+ token,
+ secure,
+ cookieDomain,
+ });
+ }
+
+ next();
+ } catch (error) {
+ appEnv.logger.debug(`Failed to authenticate: ${error}`, error);
+ res.status(401).send("Unauthorized");
+ }
+ };
+ return authMiddleware;
+};
diff --git a/source/backstage/packages/backend/src/cognito/fetchers.ts b/source/modules/acdp/backstage/packages/backend/src/cognito/fetchers.ts
similarity index 94%
rename from source/backstage/packages/backend/src/cognito/fetchers.ts
rename to source/modules/acdp/backstage/packages/backend/src/cognito/fetchers.ts
index 180115e5..812b6be5 100644
--- a/source/backstage/packages/backend/src/cognito/fetchers.ts
+++ b/source/modules/acdp/backstage/packages/backend/src/cognito/fetchers.ts
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';
+import { CognitoIdentityProvider } from "@aws-sdk/client-cognito-identity-provider";
const cognitoClient = new CognitoIdentityProvider({});
diff --git a/source/backstage/packages/backend/src/cognito/helpers.ts b/source/modules/acdp/backstage/packages/backend/src/cognito/helpers.ts
similarity index 89%
rename from source/backstage/packages/backend/src/cognito/helpers.ts
rename to source/modules/acdp/backstage/packages/backend/src/cognito/helpers.ts
index 889c72f2..367228bd 100644
--- a/source/backstage/packages/backend/src/cognito/helpers.ts
+++ b/source/modules/acdp/backstage/packages/backend/src/cognito/helpers.ts
@@ -6,13 +6,13 @@ import {
OAuthState,
ProfileInfo,
SignInResolver,
-} from '@backstage/plugin-auth-backend';
-import { InternalOAuthError } from 'passport-oauth2';
+} from "@backstage/plugin-auth-backend";
+import { InternalOAuthError } from "passport-oauth2";
-import jwtDecoder from 'jwt-decode';
-import express from 'express';
-import passport from 'passport';
-import lodash from 'lodash';
+import jwtDecoder from "jwt-decode";
+import express from "express";
+import passport from "passport";
+import lodash from "lodash";
export type PassportProfile = passport.Profile & {
avatarUrl?: string;
@@ -65,7 +65,7 @@ export const executeRedirectStrategy = async (
providerStrategy: passport.Strategy,
options: Record,
): Promise => {
- return new Promise(resolve => {
+ return new Promise((resolve) => {
const strategy = Object.create(providerStrategy);
strategy.redirect = (url: string, status?: number) => {
resolve({ url, status: status ?? undefined });
@@ -77,10 +77,10 @@ export const executeRedirectStrategy = async (
export const encodeState = (state: OAuthState): string => {
const stateString = new URLSearchParams(
- lodash.pickBy(state, value => value !== undefined),
+ lodash.pickBy(state, (value) => value !== undefined),
).toString();
- return Buffer.from(stateString, 'utf-8').toString('hex');
+ return Buffer.from(stateString, "utf-8").toString("hex");
};
export const executeFrameHandlerStrategy = async (
@@ -94,10 +94,10 @@ export const executeFrameHandlerStrategy = async (
resolve({ result, privateInfo });
};
strategy.fail = (
- info: { type: 'success' | 'error'; message?: string },
+ info: { type: "success" | "error"; message?: string },
// _status: number,
) => {
- reject(new Error(`Authentication rejected, ${info.message ?? ''}`));
+ reject(new Error(`Authentication rejected, ${info.message ?? ""}`));
};
strategy.error = (error: InternalOAuthError) => {
let message = `Authentication failed, ${error.message}`;
@@ -117,7 +117,7 @@ export const executeFrameHandlerStrategy = async (
reject(new Error(message));
};
strategy.redirect = () => {
- reject(new Error('Unexpected redirect'));
+ reject(new Error("Unexpected redirect"));
};
strategy.authenticate(req, {});
},
@@ -157,7 +157,7 @@ export const executeRefreshTokenStrategy = async (
refreshToken,
{
scope,
- grant_type: 'refresh_token',
+ grant_type: "refresh_token",
},
(
err: Error | null,
@@ -218,10 +218,9 @@ export const executeFetchUserProfileStrategy = async (
*/
export function createAuthProviderIntegration<
TCreateOptions extends unknown[],
- TResolvers extends
- | {
- [name in string]: (...args: any[]) => SignInResolver;
- },
+ TResolvers extends {
+ [name in string]: (...args: any[]) => SignInResolver;
+ },
>(config: {
create: (...args: TCreateOptions) => AuthProviderFactory;
resolvers?: TResolvers;
diff --git a/source/backstage/packages/backend/src/cognito/provider.ts b/source/modules/acdp/backstage/packages/backend/src/cognito/provider.ts
similarity index 86%
rename from source/backstage/packages/backend/src/cognito/provider.ts
rename to source/modules/acdp/backstage/packages/backend/src/cognito/provider.ts
index 07ad3cc2..fce214cb 100644
--- a/source/backstage/packages/backend/src/cognito/provider.ts
+++ b/source/modules/acdp/backstage/packages/backend/src/cognito/provider.ts
@@ -14,7 +14,7 @@ import {
OAuthResult,
OAuthStartRequest,
SignInResolver,
-} from '@backstage/plugin-auth-backend';
+} from "@backstage/plugin-auth-backend";
import {
createAuthProviderIntegration,
@@ -23,12 +23,12 @@ import {
executeRedirectStrategy,
executeRefreshTokenStrategy,
makeProfileInfo,
-} from './helpers';
+} from "./helpers";
-import { CognitoStrategy } from './strategy';
-import express from 'express';
-import { fetchClientDetails } from './fetchers';
-import { Logger } from 'winston';
+import { CognitoStrategy } from "./strategy";
+import express from "express";
+import { fetchClientDetails } from "./fetchers";
+import { Logger } from "winston";
export const cognitoDefaultAuthHandler: AuthHandler = async ({
fullProfile,
@@ -80,7 +80,7 @@ export class CognitoAuthProvider implements OAuthHandlers {
`Can't setup Cognito authentication, missing UserPoolId`,
);
throw new Error(
- 'Cognito Authentication Not Configured: missing user pool id attribute [userPoolId]',
+ "Cognito Authentication Not Configured: missing user pool id attribute [userPoolId]",
);
}
@@ -117,7 +117,7 @@ export class CognitoAuthProvider implements OAuthHandlers {
);
},
)
- .catch(ex => {
+ .catch((ex) => {
throw new Error(
`Failed to setup cognito authentication: ${ex.message}`,
);
@@ -137,7 +137,7 @@ export class CognitoAuthProvider implements OAuthHandlers {
const response = await this.handleResult(result);
req.res?.cookie(this.tokenCookie, response.backstageIdentity?.token, {
- sameSite: 'lax',
+ sameSite: "lax",
expires: new Date(
Date.now() + (response.providerInfo.expiresInSeconds || 3600) * 1000,
),
@@ -206,17 +206,17 @@ export const cognito = createAuthProviderIntegration({
};
}) {
return ({ providerId, globalConfig, config, resolverContext }) =>
- OAuthEnvironmentHandler.mapConfig(config, envConfig => {
- const userPoolId = envConfig.getString('userPoolId');
- const clientId = envConfig.getOptionalString('clientId');
- const scopes = envConfig.getOptionalStringArray('scopes');
- const customCallbackURL = envConfig.getOptionalString('callbackUrl');
+ OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
+ const userPoolId = envConfig.getString("userPoolId");
+ const clientId = envConfig.getOptionalString("clientId");
+ const scopes = envConfig.getOptionalStringArray("scopes");
+ const customCallbackURL = envConfig.getOptionalString("callbackUrl");
const tokenCookie =
- envConfig.getOptionalString('auth.cookie') || 'X-Cognito-Token';
+ envConfig.getOptionalString("auth.cookie") || "X-Cognito-Token";
options.logger.info(
`Creating Cognito Auth Provider for UserPool ${userPoolId} [ClientId: ${
- clientId ?? 'None provided'
+ clientId ?? "None provided"
}`,
);
const callbackURL =
@@ -228,7 +228,7 @@ export const cognito = createAuthProviderIntegration({
if (!userPoolId) {
throw new Error(
- 'Cognito Auth Configuration error: missing cognito user pool ID [userPoolId].',
+ "Cognito Auth Configuration error: missing cognito user pool ID [userPoolId].",
);
}
diff --git a/source/backstage/packages/backend/src/cognito/strategy.ts b/source/modules/acdp/backstage/packages/backend/src/cognito/strategy.ts
similarity index 83%
rename from source/backstage/packages/backend/src/cognito/strategy.ts
rename to source/modules/acdp/backstage/packages/backend/src/cognito/strategy.ts
index 220d3746..1d7bf2ec 100644
--- a/source/backstage/packages/backend/src/cognito/strategy.ts
+++ b/source/modules/acdp/backstage/packages/backend/src/cognito/strategy.ts
@@ -1,11 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { OutgoingHttpHeaders } from 'http';
+import { OutgoingHttpHeaders } from "http";
import OAuth2Strategy, {
StrategyOptions,
VerifyFunction,
-} from 'passport-oauth2';
+} from "passport-oauth2";
export interface CognitoStrategyOptions {
clientID: string;
@@ -30,8 +30,8 @@ export class CognitoStrategy extends OAuth2Strategy {
constructor(options: CognitoStrategyOptions, verify: VerifyFunction) {
let optionsScopes = [] as string[];
- if (typeof options.scopes === 'string') {
- optionsScopes = options.scopes.split(options.scopeSeparator || ' ');
+ if (typeof options.scopes === "string") {
+ optionsScopes = options.scopes.split(options.scopeSeparator || " ");
} else {
if (options.scopes) {
optionsScopes = options.scopes;
@@ -44,7 +44,7 @@ export class CognitoStrategy extends OAuth2Strategy {
scope: [...(options.allowedScopes || []), ...optionsScopes],
};
super(optionsWithURLs, verify);
- this.name = 'cognito';
+ this.name = "cognito";
this.options = options;
this._oauth2.useAuthorizationHeaderforGET(true);
this.userInfoURL = `https://${options.authDomain}/oauth2/userInfo`;
@@ -53,7 +53,7 @@ export class CognitoStrategy extends OAuth2Strategy {
authorizationParams() {
return {
audience: this.options.authDomain,
- prompt: 'consent',
+ prompt: "consent",
};
}
@@ -62,22 +62,22 @@ export class CognitoStrategy extends OAuth2Strategy {
if (err) {
return done(
new OAuth2Strategy.InternalOAuthError(
- 'Failed to fetch user profile',
+ "Failed to fetch user profile",
err.statusCode,
),
);
}
if (!body) {
return done(
- new Error('Failed to fetch user profile, body cannot be empty'),
+ new Error("Failed to fetch user profile, body cannot be empty"),
);
}
try {
- const json = typeof body !== 'string' ? body.toString() : body;
+ const json = typeof body !== "string" ? body.toString() : body;
const profile = CognitoStrategy.parse(json);
return done(null, profile);
} catch (e) {
- return done(new Error('Failed to parse user profile'));
+ return done(new Error("Failed to parse user profile"));
}
});
}
@@ -85,7 +85,7 @@ export class CognitoStrategy extends OAuth2Strategy {
const resp = JSON.parse(json);
return {
id: resp.account_id,
- provider: 'cognito',
+ provider: "cognito",
username: resp.nickname,
displayName: resp.name,
emails: [{ value: resp.email }],
diff --git a/source/modules/acdp/backstage/packages/backend/src/index.test.ts b/source/modules/acdp/backstage/packages/backend/src/index.test.ts
new file mode 100644
index 00000000..f220966c
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/index.test.ts
@@ -0,0 +1,11 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { PluginEnvironment } from "./types";
+
+describe("test", () => {
+ it("unbreaks the test runner", () => {
+ const unbreaker = {} as PluginEnvironment;
+ expect(unbreaker).toBeTruthy();
+ });
+});
diff --git a/source/modules/acdp/backstage/packages/backend/src/index.ts b/source/modules/acdp/backstage/packages/backend/src/index.ts
new file mode 100644
index 00000000..4b710195
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/index.ts
@@ -0,0 +1,166 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import Router from "express-promise-router";
+import { NextFunction, Request, Response, RequestHandler } from "express";
+import {
+ createServiceBuilder,
+ loadBackendConfig,
+ getRootLogger,
+ useHotMemoize,
+ notFoundHandler,
+ CacheManager,
+ DatabaseManager,
+ UrlReaders,
+ ServerTokenManager,
+ HostDiscovery,
+ createLegacyAuthAdapters,
+} from "@backstage/backend-common";
+import { TaskScheduler } from "@backstage/backend-tasks";
+import { Config } from "@backstage/config";
+import app from "./plugins/app";
+import auth from "./plugins/auth";
+import catalog from "./plugins/catalog";
+import scaffolder from "./plugins/scaffolder";
+import proxy from "./plugins/proxy";
+import techdocs from "./plugins/techdocs";
+import search from "./plugins/search";
+import acdp from "./plugins/acdp";
+
+import cookieParser from "cookie-parser";
+import { PluginEnvironment } from "./types";
+import { ServerPermissionClient } from "@backstage/plugin-permission-node";
+import { DefaultIdentityClient } from "@backstage/plugin-auth-node";
+import { createAuthMiddleware } from "./alb-auth/middleware";
+import { customErrorHandler } from "./middleware/customErrorHandler";
+import { CatalogClient } from "@backstage/catalog-client";
+import { ScmIntegrations } from "@backstage/integration";
+
+function makeCreateEnv(config: Config) {
+ const root = getRootLogger();
+ const reader = UrlReaders.default({ logger: root, config });
+ const discovery = HostDiscovery.fromConfig(config);
+ const catalogClient = new CatalogClient({
+ discoveryApi: discovery,
+ });
+ const cacheManager = CacheManager.fromConfig(config);
+ const databaseManager = DatabaseManager.fromConfig(config, { logger: root });
+ const tokenManager = ServerTokenManager.fromConfig(config, { logger: root });
+ const taskScheduler = TaskScheduler.fromConfig(config);
+ const identity = DefaultIdentityClient.create({
+ discovery,
+ algorithms: ["RS256", "ES256", "HS256"],
+ });
+ const permissions = ServerPermissionClient.fromConfig(config, {
+ discovery,
+ tokenManager,
+ });
+
+ const { auth, httpAuth } = createLegacyAuthAdapters({
+ auth: undefined,
+ httpAuth: undefined,
+ discovery: discovery,
+ identity: identity,
+ });
+
+ root.info(`Created UrlReader ${reader}`);
+
+ return (plugin: string): PluginEnvironment => {
+ const logger = root.child({ type: "plugin", plugin });
+ const database = databaseManager.forPlugin(plugin);
+ const cache = cacheManager.forPlugin(plugin);
+ const scheduler = taskScheduler.forPlugin(plugin);
+ const integrations = ScmIntegrations.fromConfig(config);
+ return {
+ auth,
+ httpAuth,
+ logger,
+ database,
+ cache,
+ catalogClient,
+ config,
+ discovery,
+ identity,
+ integrations,
+ permissions,
+ reader,
+ scheduler,
+ tokenManager,
+ };
+ };
+}
+
+async function main() {
+ const config = await loadBackendConfig({
+ argv: process.argv,
+ logger: getRootLogger(),
+ });
+ const createEnv = makeCreateEnv(config);
+
+ const catalogEnv = useHotMemoize(module, () => createEnv("catalog"));
+ const scaffolderEnv = useHotMemoize(module, () => createEnv("scaffolder"));
+ const authEnv = useHotMemoize(module, () => createEnv("auth"));
+ const proxyEnv = useHotMemoize(module, () => createEnv("proxy"));
+ const techdocsEnv = useHotMemoize(module, () => createEnv("techdocs"));
+ const searchEnv = useHotMemoize(module, () => createEnv("search"));
+ const appEnv = useHotMemoize(module, () => createEnv("app"));
+ const acdpEnv = useHotMemoize(module, () => createEnv("acdp-backend"));
+
+ let authMiddleware: RequestHandler | undefined = undefined;
+ if (authEnv.config.getOptional("auth.environment") === "development") {
+ authMiddleware = async (_: Request, __: Response, next: NextFunction) => {
+ next();
+ };
+ } else {
+ authMiddleware = await createAuthMiddleware(config, appEnv);
+ }
+
+ const customErrorHandlerMiddleware = customErrorHandler({
+ showStackTraces: false,
+ });
+
+ const apiRouter = Router();
+ apiRouter.use(cookieParser());
+ apiRouter.use("/auth", await auth(authEnv));
+ apiRouter.use("/cookie", authMiddleware, (_req, res) => {
+ res.status(200).send(`Coming right up`);
+ });
+ apiRouter.use("/scaffolder", authMiddleware, await scaffolder(scaffolderEnv));
+ apiRouter.use("/catalog", authMiddleware, await catalog(catalogEnv));
+ apiRouter.use("/techdocs", authMiddleware, await techdocs(techdocsEnv));
+ apiRouter.use("/proxy", authMiddleware, await proxy(proxyEnv));
+ apiRouter.use("/search", authMiddleware, await search(searchEnv));
+ apiRouter.use("/acdp-backend", authMiddleware, await acdp(acdpEnv));
+
+ apiRouter.use(authMiddleware, notFoundHandler());
+ // customErrorHandlerMiddleware must be the last middleware to function
+ apiRouter.use(customErrorHandlerMiddleware);
+
+ const service = createServiceBuilder(module)
+ .loadConfig(config)
+ .addRouter("/api", apiRouter)
+ .addRouter("", await app(appEnv));
+
+ await service.start().catch((err) => {
+ console.log(err);
+ process.exit(1);
+ });
+}
+
+module.hot?.accept();
+main().catch((error) => {
+ console.error("Backend failed to start up", error);
+ process.exit(1);
+});
+
+declare global {
+ namespace Express {
+ interface User {
+ token?: string;
+ fullProfile?: any;
+ accessToken?: string;
+ refreshToken?: string;
+ params?: any;
+ }
+ }
+}
diff --git a/source/modules/acdp/backstage/packages/backend/src/middleware/customErrorHandler.test.ts b/source/modules/acdp/backstage/packages/backend/src/middleware/customErrorHandler.test.ts
new file mode 100644
index 00000000..7efe6f22
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/middleware/customErrorHandler.test.ts
@@ -0,0 +1,218 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import express from "express";
+import request from "supertest";
+import { customErrorHandler } from "./customErrorHandler";
+import {
+ AuthenticationError,
+ ConflictError,
+ InputError,
+ NotAllowedError,
+ NotFoundError,
+ NotModifiedError,
+} from "@backstage/errors";
+import createError from "http-errors";
+
+type ErrorCause = {
+ name: string;
+ message: string;
+ stack: string;
+};
+
+class CustomError extends Error {
+ cause: ErrorCause;
+ constructor(message: string, stack: string) {
+ super(message);
+ this.name = "CustomError";
+ this.cause = { name: "CustomError", message: message, stack: stack };
+ this.stack = stack;
+ }
+}
+
+describe("customErrorHandler", () => {
+ let app: express.Application;
+
+ beforeEach(function () {
+ app = express();
+ });
+
+ it("gives default code and message", async () => {
+ app.use("/breaks", () => {
+ throw new Error("some message");
+ });
+ app.use(customErrorHandler());
+
+ const response = await request(app).get("/breaks");
+
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({
+ error: expect.objectContaining({
+ name: "Error",
+ message: "some message",
+ }),
+ request: { method: "GET", url: "/breaks" },
+ response: { statusCode: 500 },
+ });
+ });
+
+ it("does not try to send the response again if it has already been sent", async () => {
+ const mockSend = jest.fn();
+
+ app.use("/works_with_async_fail", (_, res) => {
+ res.status(200).send("hello");
+
+ // mutate the response object to test the middleware.
+ // it's hard to catch errors inside middleware from the outside.
+ res.send = mockSend;
+ throw new Error("some message");
+ });
+
+ app.use(customErrorHandler());
+ const response = await request(app).get("/works_with_async_fail");
+
+ expect(response.status).toBe(200);
+ expect(response.text).toBe("hello");
+
+ expect(mockSend).not.toHaveBeenCalled();
+ });
+
+ it("takes code from http-errors library errors", async () => {
+ app.use("/breaks", () => {
+ throw createError(432, "Some Message");
+ });
+ app.use(customErrorHandler());
+
+ const response = await request(app).get("/breaks");
+
+ expect(response.status).toBe(432);
+ expect(response.body).toEqual({
+ error: {
+ expose: true,
+ name: "BadRequestError",
+ message: "Some Message",
+ status: 432,
+ statusCode: 432,
+ },
+ request: {
+ method: "GET",
+ url: "/breaks",
+ },
+ response: { statusCode: 432 },
+ });
+ });
+
+ it.each([
+ ["/NotModifiedError", NotModifiedError, 304],
+ ["/InputError", InputError, 400],
+ ["/AuthenticationError", AuthenticationError, 401],
+ ["/NotAllowedError", NotAllowedError, 403],
+ ["/NotFoundError", NotFoundError, 404],
+ ["/ConflictError", ConflictError, 409],
+ ])("handles well-known error classes", async (path, error, statusCode) => {
+ app.use(path, () => {
+ throw new error();
+ });
+ app.use(customErrorHandler());
+
+ const r = request(app);
+
+ expect((await r.get(path)).status).toBe(statusCode);
+ if (statusCode != 304) {
+ expect((await r.get(path)).body.error.name).toBe(error.name);
+ }
+ });
+
+ it("logs all 500 errors", async () => {
+ const mockLogger = { child: jest.fn(), error: jest.fn() };
+ mockLogger.child.mockImplementation(() => mockLogger as any);
+
+ const thrownError = new Error("some error");
+
+ app.use("/breaks", () => {
+ throw thrownError;
+ });
+ app.use(customErrorHandler({ logger: mockLogger as any }));
+
+ await request(app).get("/breaks");
+
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ "Request failed with status 500",
+ thrownError,
+ );
+ });
+
+ it("does not log 400 errors", async () => {
+ const mockLogger = { child: jest.fn(), error: jest.fn() };
+ mockLogger.child.mockImplementation(() => mockLogger as any);
+
+ app.use("/NotFound", () => {
+ throw new NotFoundError();
+ });
+ app.use(customErrorHandler({ logger: mockLogger as any }));
+
+ await request(app).get("/NotFound");
+
+ expect(mockLogger.error).not.toHaveBeenCalled();
+ });
+
+ it("log 400 errors when logClientErrors is true", async () => {
+ const mockLogger = { child: jest.fn(), error: jest.fn() };
+ mockLogger.child.mockImplementation(() => mockLogger as any);
+
+ app.use("/NotFound", () => {
+ throw new NotFoundError();
+ });
+ app.use(
+ customErrorHandler({ logger: mockLogger as any, logClientErrors: true }),
+ );
+
+ await request(app).get("/NotFound");
+
+ expect(mockLogger.error).toHaveBeenCalled();
+ });
+
+ it("dont show stack trace from error", async () => {
+ app.use("/breaks", () => {
+ throw new CustomError("some message", "DANGEROUS STACK TRACE");
+ });
+ app.use(customErrorHandler({ showStackTraces: false }));
+
+ const response = await request(app).get("/breaks");
+
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({
+ error: {
+ name: "CustomError",
+ message: "some message",
+ },
+ request: { method: "GET", url: "/breaks" },
+ response: { statusCode: 500 },
+ });
+ });
+
+ it("shows stack trace from error", async () => {
+ app.use("/breaks", () => {
+ throw new CustomError("some message", "DANGEROUS STACK TRACE");
+ });
+ app.use(customErrorHandler({ showStackTraces: true }));
+
+ const response = await request(app).get("/breaks");
+
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({
+ error: {
+ name: "CustomError",
+ message: "some message",
+ stack: "DANGEROUS STACK TRACE",
+ cause: {
+ name: "CustomError",
+ message: "some message",
+ stack: "DANGEROUS STACK TRACE",
+ },
+ },
+ request: { method: "GET", url: "/breaks" },
+ response: { statusCode: 500 },
+ });
+ });
+});
diff --git a/source/backstage/packages/backend/src/middleware/customErrorHandler.ts b/source/modules/acdp/backstage/packages/backend/src/middleware/customErrorHandler.ts
similarity index 84%
rename from source/backstage/packages/backend/src/middleware/customErrorHandler.ts
rename to source/modules/acdp/backstage/packages/backend/src/middleware/customErrorHandler.ts
index c239f3f7..e56c4aa9 100644
--- a/source/backstage/packages/backend/src/middleware/customErrorHandler.ts
+++ b/source/modules/acdp/backstage/packages/backend/src/middleware/customErrorHandler.ts
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { Request, Response, ErrorRequestHandler, NextFunction } from 'express';
+import { Request, Response, ErrorRequestHandler, NextFunction } from "express";
import {
AuthenticationError,
ConflictError,
@@ -13,18 +13,17 @@ import {
ServiceUnavailableError,
NotImplementedError,
serializeError,
-} from '@backstage/errors';
-import { getRootLogger } from '@backstage/backend-common';
-import { ErrorHandlerOptions } from '@backstage/backend-common';
+} from "@backstage/errors";
+import { getRootLogger, ErrorHandlerOptions } from "@backstage/backend-common";
export function customErrorHandler(
options: ErrorHandlerOptions = {},
): ErrorRequestHandler {
const showStackTraces =
- options.showStackTraces ?? process.env.NODE_ENV === 'development';
+ options.showStackTraces ?? process.env.NODE_ENV === "development";
const logger = (options.logger ?? getRootLogger()).child({
- type: 'errorHandler',
+ type: "errorHandler",
});
return (error: Error, req: Request, res: Response, next: NextFunction) => {
@@ -39,7 +38,9 @@ export function customErrorHandler(
next(error);
return;
}
- const serializedError = serializeError(error, {includeStack: showStackTraces});
+ const serializedError = serializeError(error, {
+ includeStack: showStackTraces,
+ });
if (!showStackTraces) {
delete serializedError.stack;
@@ -58,16 +59,16 @@ export function customErrorHandler(
};
res.status(statusCode).json(body);
-};
+ };
}
function getStatusCode(error: Error): number {
// Look for common http library status codes
- const knownStatusCodeFields = ['statusCode', 'status'];
+ const knownStatusCodeFields = ["statusCode", "status"];
for (const field of knownStatusCodeFields) {
const statusCode = (error as any)[field];
if (
- typeof statusCode === 'number' &&
+ typeof statusCode === "number" &&
(statusCode | 0) === statusCode && // is whole integer
statusCode >= 100 &&
statusCode <= 599
diff --git a/source/modules/acdp/backstage/packages/backend/src/plugins/acdp.ts b/source/modules/acdp/backstage/packages/backend/src/plugins/acdp.ts
new file mode 100644
index 00000000..1ea68a12
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/plugins/acdp.ts
@@ -0,0 +1,35 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { PluginEnvironment } from "../types";
+import { DefaultAwsCredentialsManager } from "@backstage/integration-aws-node";
+import {
+ createRouter,
+ AcdpBuildApi,
+ AcdpBuildService,
+} from "backstage-plugin-acdp-backend";
+import { CatalogClient } from "@backstage/catalog-client";
+import { ScmIntegrations } from "@backstage/integration";
+
+export default async function createPlugin(env: PluginEnvironment) {
+ const credsManager = DefaultAwsCredentialsManager.fromConfig(env.config);
+ const catalogClient = new CatalogClient({
+ discoveryApi: env.discovery,
+ });
+
+ const integrations = ScmIntegrations.fromConfig(env.config);
+
+ const acdpBuildService = new AcdpBuildService({
+ config: env.config,
+ reader: env.reader,
+ integrations: integrations,
+ awsCredentialsProvider: await credsManager.getCredentialProvider(),
+ logger: env.logger,
+ });
+ const acdpBuildApi = new AcdpBuildApi(catalogClient, acdpBuildService);
+ return await createRouter({
+ logger: env.logger,
+ config: env.config,
+ acdpBuildApi: acdpBuildApi,
+ });
+}
diff --git a/source/modules/acdp/backstage/packages/backend/src/plugins/app.ts b/source/modules/acdp/backstage/packages/backend/src/plugins/app.ts
new file mode 100644
index 00000000..b4da82b5
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/plugins/app.ts
@@ -0,0 +1,17 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { createRouter } from "@backstage/plugin-app-backend";
+import { Router } from "express";
+import { PluginEnvironment } from "../types";
+
+export default async function createPlugin(
+ env: PluginEnvironment,
+): Promise {
+ return await createRouter({
+ logger: env.logger,
+ config: env.config,
+ database: env.database,
+ appPackageName: "app",
+ });
+}
diff --git a/source/backstage/packages/backend/src/plugins/auth.ts b/source/modules/acdp/backstage/packages/backend/src/plugins/auth.ts
similarity index 75%
rename from source/backstage/packages/backend/src/plugins/auth.ts
rename to source/modules/acdp/backstage/packages/backend/src/plugins/auth.ts
index be0bb15c..508d1352 100644
--- a/source/backstage/packages/backend/src/plugins/auth.ts
+++ b/source/modules/acdp/backstage/packages/backend/src/plugins/auth.ts
@@ -4,14 +4,14 @@
import {
createRouter,
defaultAuthProviderFactories,
-} from '@backstage/plugin-auth-backend';
-import { Router } from 'express';
-import { PluginEnvironment } from '../types';
-import { createCognitoProvider } from '../cognito/provider';
+} from "@backstage/plugin-auth-backend";
+import { Router } from "express";
+import { PluginEnvironment } from "../types";
+import { createCognitoProvider } from "../cognito/provider";
import {
DEFAULT_NAMESPACE,
stringifyEntityRef,
-} from '@backstage/catalog-model';
+} from "@backstage/catalog-model";
export default async function createPlugin(
env: PluginEnvironment,
@@ -22,7 +22,7 @@ export default async function createPlugin(
database: env.database,
discovery: env.discovery,
tokenManager: env.tokenManager,
- tokenFactoryAlgorithm: 'RS256',
+ tokenFactoryAlgorithm: "RS256",
providerFactories: {
...defaultAuthProviderFactories,
cognito: createCognitoProvider({
@@ -33,13 +33,13 @@ export default async function createPlugin(
profile: { email },
} = info;
if (!email) {
- throw new Error('User profile contained no email');
+ throw new Error("User profile contained no email");
}
- const [backstageUsername] = email.split('@');
+ const [backstageUsername] = email.split("@");
const userEntityRef = stringifyEntityRef({
- kind: 'User',
+ kind: "User",
name: backstageUsername,
namespace: DEFAULT_NAMESPACE,
});
diff --git a/source/modules/acdp/backstage/packages/backend/src/plugins/catalog.ts b/source/modules/acdp/backstage/packages/backend/src/plugins/catalog.ts
new file mode 100644
index 00000000..9258839b
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/plugins/catalog.ts
@@ -0,0 +1,24 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { CatalogBuilder } from "@backstage/plugin-catalog-backend";
+import { ScaffolderEntitiesProcessor } from "@backstage/plugin-catalog-backend-module-scaffolder-entity-model";
+import { AwsS3EntityProvider } from "@backstage/plugin-catalog-backend-module-aws";
+import { Router } from "express";
+import { PluginEnvironment } from "../types";
+
+export default async function createPlugin(
+ env: PluginEnvironment,
+): Promise {
+ const builder = CatalogBuilder.create(env);
+ builder.addEntityProvider(
+ AwsS3EntityProvider.fromConfig(env.config, {
+ logger: env.logger,
+ scheduler: env.scheduler,
+ }),
+ );
+ builder.addProcessor(new ScaffolderEntitiesProcessor());
+ const { processingEngine, router } = await builder.build();
+ await processingEngine.start();
+ return router;
+}
diff --git a/source/modules/acdp/backstage/packages/backend/src/plugins/proxy.ts b/source/modules/acdp/backstage/packages/backend/src/plugins/proxy.ts
new file mode 100644
index 00000000..1003a488
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/plugins/proxy.ts
@@ -0,0 +1,16 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { createRouter } from "@backstage/plugin-proxy-backend";
+import { Router } from "express";
+import { PluginEnvironment } from "../types";
+
+export default async function createPlugin(
+ env: PluginEnvironment,
+): Promise {
+ return await createRouter({
+ logger: env.logger,
+ config: env.config,
+ discovery: env.discovery,
+ });
+}
diff --git a/source/modules/acdp/backstage/packages/backend/src/plugins/scaffolder.ts b/source/modules/acdp/backstage/packages/backend/src/plugins/scaffolder.ts
new file mode 100644
index 00000000..fcc98335
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/plugins/scaffolder.ts
@@ -0,0 +1,60 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Router } from "express";
+import type { PluginEnvironment } from "../types";
+import {
+ createBuiltinActions,
+ createRouter,
+} from "@backstage/plugin-scaffolder-backend";
+import {
+ createAcdpConfigureAction,
+ createAcdpCatalogCreateAction,
+ createNewYamlFileAction,
+} from "backstage-plugin-acdp-backend";
+
+export default async function createPlugin(
+ env: PluginEnvironment,
+): Promise {
+ const builtInActions = createBuiltinActions({
+ catalogClient: env.catalogClient,
+ config: env.config,
+ integrations: env.integrations,
+ reader: env.reader,
+ });
+
+ const actions = [
+ ...builtInActions,
+ await createAcdpCatalogCreateAction({
+ config: env.config,
+ reader: env.reader,
+ integrations: env.integrations,
+ catalogClient: env.catalogClient,
+ discovery: env.discovery,
+ tokenManager: env.tokenManager,
+ logger: env.logger,
+ }),
+ await createAcdpConfigureAction({
+ config: env.config,
+ reader: env.reader,
+ integrations: env.integrations,
+ catalogClient: env.catalogClient,
+ tokenManager: env.tokenManager,
+ logger: env.logger,
+ }),
+ createNewYamlFileAction(),
+ ];
+
+ return await createRouter({
+ logger: env.logger,
+ config: env.config,
+ database: env.database,
+ reader: env.reader,
+ catalogClient: env.catalogClient,
+ actions: actions,
+ identity: env.identity,
+ permissions: env.permissions,
+ auth: env.auth,
+ httpAuth: env.httpAuth,
+ });
+}
diff --git a/source/backstage/packages/backend/src/plugins/search.ts b/source/modules/acdp/backstage/packages/backend/src/plugins/search.ts
similarity index 78%
rename from source/backstage/packages/backend/src/plugins/search.ts
rename to source/modules/acdp/backstage/packages/backend/src/plugins/search.ts
index e3fa1343..e0bb2a2c 100644
--- a/source/backstage/packages/backend/src/plugins/search.ts
+++ b/source/modules/acdp/backstage/packages/backend/src/plugins/search.ts
@@ -1,16 +1,16 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { useHotCleanup } from '@backstage/backend-common';
-import { createRouter } from '@backstage/plugin-search-backend';
+import { useHotCleanup } from "@backstage/backend-common";
+import { createRouter } from "@backstage/plugin-search-backend";
import {
IndexBuilder,
LunrSearchEngine,
-} from '@backstage/plugin-search-backend-node';
-import { PluginEnvironment } from '../types';
-import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend';
-import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
-import { Router } from 'express';
+} from "@backstage/plugin-search-backend-node";
+import { PluginEnvironment } from "../types";
+import { DefaultCatalogCollatorFactory } from "@backstage/plugin-search-backend-module-catalog";
+import { DefaultTechDocsCollatorFactory } from "@backstage/plugin-search-backend-module-techdocs";
+import { Router } from "express";
export default async function createPlugin(
env: PluginEnvironment,
@@ -65,5 +65,7 @@ export default async function createPlugin(
permissions: env.permissions,
config: env.config,
logger: env.logger,
+ auth: env.auth,
+ httpAuth: env.httpAuth,
});
}
diff --git a/source/modules/acdp/backstage/packages/backend/src/plugins/techdocs.ts b/source/modules/acdp/backstage/packages/backend/src/plugins/techdocs.ts
new file mode 100644
index 00000000..49f92d1f
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/plugins/techdocs.ts
@@ -0,0 +1,74 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ createRouter,
+ Generators,
+ Preparers,
+ Publisher,
+ TechdocsGenerator,
+} from "@backstage/plugin-techdocs-backend";
+import { Router } from "express";
+import { PluginEnvironment } from "../types";
+import { Config } from "@backstage/config";
+import { Entity } from "@backstage/catalog-model";
+import { DocsBuildStrategy } from "@backstage/plugin-techdocs-node";
+
+export class AnnotationBasedBuildStrategy implements DocsBuildStrategy {
+ private readonly config: Config;
+
+ constructor(config: Config) {
+ this.config = config;
+ }
+
+ async shouldBuild(params: { entity: Entity }): Promise {
+ let shouldBuildAnnotation =
+ params.entity.metadata?.annotations?.["aws.amazon.com/techdocs-builder"];
+
+ if (shouldBuildAnnotation !== undefined)
+ return shouldBuildAnnotation === "local";
+
+ return this.config.getString("techdocs.builder") === "local";
+ }
+}
+
+export default async function createPlugin(
+ env: PluginEnvironment,
+): Promise {
+ // Preparers are responsible for fetching source files for documentation.
+ const preparers = await Preparers.fromConfig(env.config, {
+ logger: env.logger,
+ reader: env.reader,
+ });
+
+ const generators = new Generators();
+
+ const techdocsGenerator = TechdocsGenerator.fromConfig(env.config, {
+ logger: env.logger,
+ });
+ generators.register("techdocs", techdocsGenerator);
+
+ // Publisher is used for
+ // 1. Publishing generated files to storage
+ // 2. Fetching files from storage and passing them to TechDocs frontend.
+ const publisher = await Publisher.fromConfig(env.config, {
+ logger: env.logger,
+ discovery: env.discovery,
+ });
+
+ // checks if the publisher is working and logs the result
+ await publisher.getReadiness();
+
+ return await createRouter({
+ preparers,
+ generators,
+ publisher,
+ logger: env.logger,
+ config: env.config,
+ discovery: env.discovery,
+ cache: env.cache,
+ docsBuildStrategy: new AnnotationBasedBuildStrategy(env.config),
+ auth: env.auth,
+ httpAuth: env.httpAuth,
+ });
+}
diff --git a/source/modules/acdp/backstage/packages/backend/src/types.ts b/source/modules/acdp/backstage/packages/backend/src/types.ts
new file mode 100644
index 00000000..627427db
--- /dev/null
+++ b/source/modules/acdp/backstage/packages/backend/src/types.ts
@@ -0,0 +1,35 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Logger } from "winston";
+import { Config } from "@backstage/config";
+import {
+ PluginCacheManager,
+ PluginDatabaseManager,
+ PluginEndpointDiscovery,
+ TokenManager,
+ UrlReader,
+} from "@backstage/backend-common";
+import { PluginTaskScheduler } from "@backstage/backend-tasks";
+import { PermissionEvaluator } from "@backstage/plugin-permission-common";
+import { IdentityApi } from "@backstage/plugin-auth-node";
+import { CatalogClient } from "@backstage/catalog-client";
+import { ScmIntegrations } from "@backstage/integration";
+import { AuthService, HttpAuthService } from "@backstage/backend-plugin-api";
+
+export type PluginEnvironment = {
+ auth: AuthService;
+ httpAuth: HttpAuthService;
+ logger: Logger;
+ database: PluginDatabaseManager;
+ cache: PluginCacheManager;
+ catalogClient: CatalogClient;
+ config: Config;
+ discovery: PluginEndpointDiscovery;
+ identity: IdentityApi;
+ integrations: ScmIntegrations;
+ permissions: PermissionEvaluator;
+ reader: UrlReader;
+ scheduler: PluginTaskScheduler;
+ tokenManager: TokenManager;
+};
diff --git a/source/backstage/plugins/README.md b/source/modules/acdp/backstage/plugins/README.md
similarity index 100%
rename from source/backstage/plugins/README.md
rename to source/modules/acdp/backstage/plugins/README.md
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/.eslintrc.js b/source/modules/acdp/backstage/plugins/acdp-backend/.eslintrc.js
new file mode 100644
index 00000000..709e25dd
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/.eslintrc.js
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+module.exports = require("@backstage/cli/config/eslint-factory")(__dirname);
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/README.md b/source/modules/acdp/backstage/plugins/acdp-backend/README.md
new file mode 100644
index 00000000..791c3624
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/README.md
@@ -0,0 +1,14 @@
+# acdp
+
+Welcome to the acdp backend plugin!
+
+_This plugin was created through the Backstage CLI_
+
+## Getting started
+
+Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn
+start` in the root directory, and then navigating to [/acdp](http://localhost:3000/acdp).
+
+You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
+This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
+It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/package.json b/source/modules/acdp/backstage/plugins/acdp-backend/package.json
new file mode 100644
index 00000000..86817018
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/package.json
@@ -0,0 +1,74 @@
+{
+ "name": "backstage-plugin-acdp-backend",
+ "description": "ACDP Backend plugin for Backstage",
+ "version": "1.1.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test --coverage",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@aws-sdk/client-codebuild": "^3.515.0",
+ "@aws-sdk/util-arn-parser": "^3.495.0",
+ "@aws-sdk/client-ssm": "^3.515.0",
+ "@aws-sdk/lib-storage": "^3.515.0",
+ "@backstage/backend-common": "^0.21.3",
+ "@backstage/catalog-model": "^1.4.4",
+ "@backstage/config": "^1.1.1",
+ "@backstage/types": "^1.1.1",
+ "@backstage/errors": "^1.2.3",
+ "@backstage/integration": "^1.9.0",
+ "@backstage/integration-aws-node": "^0.1.9",
+ "@backstage/plugin-scaffolder-node-test-utils": "^0.1.0",
+ "@backstage/plugin-techdocs-node": "^1.11.5",
+ "backstage-plugin-acdp-common": "*",
+ "@types/express": "*",
+ "express": "^4.17.1",
+ "express-promise-router": "^4.1.0",
+ "node-fetch": "^2.6.7",
+ "p-limit": "^3.1.0",
+ "recursive-readdir": "^2.2.2",
+ "prettier": "^3.1.0",
+ "winston": "^3.2.1",
+ "yn": "^4.0.0",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.25.2",
+ "@types/supertest": "^2.0.12",
+ "@types/recursive-readdir": "*",
+ "aws-sdk-client-mock": "^3.0.0",
+ "msw": "^1.0.0",
+ "supertest": "^6.2.4",
+ "ts-jest": "^29.1.1"
+ },
+ "files": [
+ "dist"
+ ],
+ "jest": {
+ "transform": {
+ "^.+\\.(ts|tsx)$": "ts-jest"
+ },
+ "coveragePathIgnorePatterns": [
+ "/index.ts",
+ "/run.ts",
+ "/service/standaloneServer.ts"
+ ]
+ }
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/__mocks__/common-mocks.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/__mocks__/common-mocks.ts
new file mode 100644
index 00000000..4456b859
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/__mocks__/common-mocks.ts
@@ -0,0 +1,296 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ PluginEndpointDiscovery,
+ TokenManager,
+ UrlReader,
+} from "@backstage/backend-common";
+import {
+ CatalogClient,
+ CatalogRequestOptions,
+} from "@backstage/catalog-client";
+import {
+ CompoundEntityRef,
+ Entity,
+ stringifyEntityRef,
+} from "@backstage/catalog-model";
+import { ConfigReader } from "@backstage/config";
+import { AuthenticationError } from "@backstage/errors";
+import { ScmIntegrations } from "@backstage/integration";
+import { AwsCredentialProvider } from "@backstage/integration-aws-node";
+
+export const mockedConfigData = {
+ acdp: {
+ s3Catalog: {
+ bucketName: "bucket",
+ prefix: "test/backstage/catalog",
+ region: "us-west-2",
+ },
+ buildConfig: {
+ buildConfigStoreSsmPrefix: "/local/backstage/acdp-build",
+ },
+ deploymentDefaults: {
+ region: "us-west-2",
+ accountId: "111111111111",
+ codeBuildProjectArn:
+ "arn:aws:codebuild:us-west-2:111111111111:project/test",
+ },
+ metrics: {
+ userAgentString: "local-user-agent",
+ },
+ },
+ techdocs: {
+ generator: {
+ runIn: "local",
+ },
+ builder: "local",
+ publisher: {
+ type: "awsS3",
+ awsS3: {
+ bucketName: "bucket",
+ region: "us-west-2",
+ bucketRootPath: "test/backstage/techdocs",
+ },
+ },
+ },
+};
+
+export const mockedCatalogEntity = {
+ apiVersion: "backstage.io/v1alpha1",
+ kind: "Component",
+ metadata: {
+ uid: "uniqueId",
+ annotations: {
+ "aws.amazon.com/acdp-deploy-on-create": "true",
+ "aws.amazon.com/acdp-deployment-target": "default",
+ "aws.amazon.com/techdocs-builder": "external",
+ "backstage.io/techdocs-ref": "dir:.",
+ "aws.amazon.com/template-entity-ref": "template:default/cms-sample",
+ "aws.amazon.com/acdp-assets-ref": "dir:assets",
+ "backstage.io/source-location":
+ "url:https://test-bucket.s3.us-west-2.amazonaws.com/local/backstage/catalog/acdp/component/cms-sample/assets/",
+ },
+ description:
+ "A CDK Python app for showing a basic skeleton for a CMS module",
+ name: "cms-sample",
+ namespace: "acdp",
+ },
+ spec: {
+ lifecycle: "experimental",
+ owner: "group:default/asdf",
+ type: "service",
+ },
+};
+
+export const mockTemplateCatalogCreateInput = {
+ assetsSourcePath: "dir:../acdp/cms-sample/",
+ componentId: "cms-sample",
+ docsSiteSourcePath: "dir:../docs/components/cms-sample/site/",
+ entity: {
+ apiVersion: "backstage.io/v1alpha1",
+ kind: "Component",
+ metadata: {
+ annotations: {
+ "aws.amazon.com/acdp-deploy-on-create": "true",
+ },
+ description: "sample description",
+ name: "cms-sample",
+ namespace: "acdp",
+ },
+ spec: {
+ lifecycle: "experimental",
+ owner: "test",
+ type: "service",
+ },
+ },
+};
+
+export const mockedTemplateEntity = {
+ apiVersion: "scaffolder.backstage.io/v1beta3",
+ kind: "Template",
+ metadata: {
+ description:
+ "A CDK Python app for showing a basic skeleton for a CMS module",
+ name: "cms-sample",
+ tags: ["cms", "guide", "sample"],
+ title: "CMS Sample Module",
+ },
+ spec: {
+ output: {
+ links: [
+ {
+ entityRef: stringifyEntityRef(mockedCatalogEntity),
+ icon: "catalog",
+ title: "Open in catalog",
+ },
+ ],
+ },
+ owner: "aws solutions",
+ parameters: [
+ {
+ properties: {
+ componentId: {
+ default: "cms-sample",
+ description: "Unique name of the component",
+ pattern: "[a-zA-Z][-a-zA-Z0-9]*[a-zA-Z]",
+ title: "Name",
+ type: "string",
+ "ui:field": "EntityNamePicker",
+ },
+ description: {
+ default:
+ "A CDK Python app for showing a basic skeleton for a CMS module",
+ description: "Help others understand what this component is for.",
+ title: "Description",
+ type: "string",
+ },
+ owner: {
+ description: "Owner of the component",
+ title: "Owner",
+ type: "string",
+ "ui:field": "OwnerPicker",
+ "ui:options": {
+ catalogFilter: {
+ kind: ["Group", "User"],
+ },
+ },
+ },
+ },
+ required: ["componentId", "owner"],
+ title: "Provide the required information",
+ },
+ {
+ properties: {
+ appUniqueId: {
+ default: "cms",
+ description:
+ "Application unique identifier used to uniquely name resources within the stack",
+ title: "App Unique ID",
+ type: "string",
+ "ui:disabled": true,
+ },
+ },
+ required: ["appUniqueId"],
+ title: "Provide the Module Configuration",
+ },
+ ],
+ steps: [
+ {
+ action: "aws:acdp:catalog:create",
+ id: "acdpCatalogCreate",
+ input: mockTemplateCatalogCreateInput,
+ name: "ACDP S3 Catalog Write",
+ },
+ {
+ action: "catalog:register",
+ id: "catalogRegister",
+ input: {
+ catalogInfoUrl: "https://test",
+ },
+ name: "Backstage Catalog Register",
+ },
+ {
+ action: "aws:acdp:configure",
+ id: "acdpConfigureDeploy",
+ input: {
+ buildParameters: [
+ {
+ name: "CFN_TEMPLATE_URL",
+ value:
+ "https://acdp-assets.s3.us-west-2.amazonaws.com/connected-mobility-solution-on-aws/v0.0.0/cms-sample/cms-sample.template",
+ },
+ {
+ name: "APP_UNIQUE_ID",
+ value: "cms",
+ },
+ ],
+ entityRef: "dummy",
+ },
+ name: "ACDP Deploy",
+ },
+ ],
+ type: "service",
+ },
+};
+
+export const mockConfig = new ConfigReader(mockedConfigData);
+
+export const mockCredentialsProvider = {
+ sdkCredentialProvider: jest.fn().mockResolvedValue({
+ accessKeyId: "asdfasdf",
+ secretAccessKey: "asdfasdf",
+ sessionToken: "asdfasdf",
+ }),
+} satisfies AwsCredentialProvider;
+
+export const mockUrlReader: jest.Mocked = {
+ readUrl: jest.fn(),
+ readTree: jest.fn(),
+ search: jest.fn(),
+};
+
+export const mockCatalogClient = (
+ entity?: Entity,
+): jest.Mocked => {
+ const mock = {
+ getEntityByRef: jest.fn(),
+ getLocationById: jest.fn(),
+ };
+ if (entity) {
+ const determineReturn = async (
+ inputEntityRef: string | CompoundEntityRef,
+ _?: CatalogRequestOptions,
+ ) => {
+ if (
+ (typeof inputEntityRef === "string" &&
+ stringifyEntityRef(entity) === inputEntityRef) ||
+ stringifyEntityRef(entity) ===
+ stringifyEntityRef(inputEntityRef as CompoundEntityRef)
+ ) {
+ return entity;
+ }
+
+ return undefined;
+ };
+ mock.getEntityByRef.mockImplementation(
+ (inputEntityRef, catalogRequestOptions) =>
+ determineReturn(inputEntityRef, catalogRequestOptions),
+ );
+ }
+ return mock as Partial<
+ jest.Mocked
+ > as jest.Mocked;
+};
+
+export const mockIntegrations = ScmIntegrations.fromConfig(mockConfig);
+
+export const mockDiscovery: jest.Mocked = {
+ getBaseUrl: jest.fn().mockResolvedValue("http://localhost:8080/api/acdp"),
+ getExternalBaseUrl: jest.fn(),
+};
+
+const mockTestToken = "test-token";
+export const mockTokenManager: jest.Mocked = {
+ authenticate: jest.fn().mockImplementation((token) => {
+ if (token !== mockTestToken)
+ throw new AuthenticationError("Token mismatch");
+ }),
+ getToken: jest.fn().mockResolvedValue(mockTestToken),
+};
+
+export function resetMocks() {
+ mockUrlReader.readUrl.mockReset();
+ mockUrlReader.readTree.mockReset();
+ mockUrlReader.search.mockReset();
+
+ setupMocks();
+}
+
+export function setupMocks() {
+ mockUrlReader.readUrl.mockResolvedValue({
+ buffer: jest
+ .fn()
+ .mockResolvedValue(Buffer.from(JSON.stringify({ a: ["b", 7] }), "utf-8")),
+ });
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-catalog-create.test.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-catalog-create.test.ts
new file mode 100644
index 00000000..b5235078
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-catalog-create.test.ts
@@ -0,0 +1,115 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { getVoidLogger } from "@backstage/backend-common";
+import { mockClient } from "aws-sdk-client-mock";
+import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
+import { CatalogClient } from "@backstage/catalog-client";
+import { fetchContents } from "@backstage/plugin-scaffolder-node";
+
+import { createAcdpCatalogCreateAction } from ".";
+import {
+ mockDiscovery,
+ mockUrlReader,
+ mockConfig,
+ mockIntegrations,
+ mockedTemplateEntity,
+ mockTemplateCatalogCreateInput,
+ mockedCatalogEntity,
+ mockTokenManager,
+} from "../__mocks__/common-mocks";
+import { stringifyEntityRef } from "@backstage/catalog-model";
+import { Publisher, PublisherBase } from "@backstage/plugin-techdocs-node";
+import { createMockDirectory } from "@backstage/backend-test-utils";
+import { createMockActionContext } from "@backstage/plugin-scaffolder-node-test-utils";
+
+const mockedS3Client = mockClient(S3Client);
+jest.mock("../service/acdp-build-service");
+
+jest.mock("@backstage/plugin-scaffolder-node", () => ({
+ ...jest.requireActual("@backstage/plugin-scaffolder-node"),
+ fetchContents: jest.fn(),
+ fetch: jest.fn(),
+}));
+
+jest.mock("@backstage/plugin-techdocs-node");
+jest.mock("../utils/aws-s3-helper");
+
+const mockPublisherBase = (): jest.Mocked => {
+ const mock = {
+ publish: jest.fn(),
+ getReadiness: jest.fn(),
+ };
+
+ mock.publish.mockImplementation(() => Promise.resolve({}));
+ mock.getReadiness.mockImplementation(() => Promise.resolve({}));
+
+ return mock as Partial<
+ jest.Mocked
+ > as jest.Mocked;
+};
+
+Publisher.fromConfig = async () => mockPublisherBase();
+
+beforeEach(() => {
+ mockedS3Client.reset();
+});
+
+describe("createAcdpCatalogCreateAction", () => {
+ const workspacePath = createMockDirectory().resolve("/tmp");
+
+ it("", async () => {
+ mockedS3Client.on(PutObjectCommand).resolves({
+ ETag: "test",
+ });
+
+ const mockCatalogClient = (): jest.Mocked => {
+ const mock = {
+ getEntityByRef: jest.fn(),
+ getLocationById: jest.fn(),
+ };
+
+ const determineReturn = (inputEntityRef: string) => {
+ if (stringifyEntityRef(mockedCatalogEntity) === inputEntityRef) {
+ return mockedCatalogEntity;
+ }
+ return undefined;
+ };
+ mock.getEntityByRef.mockImplementationOnce(async () => undefined);
+ mock.getEntityByRef.mockImplementation(determineReturn);
+
+ return mock as Partial<
+ jest.Mocked
+ > as jest.Mocked;
+ };
+
+ const catalogClient = mockCatalogClient();
+
+ const mockContext = createMockActionContext({
+ templateInfo: {
+ baseUrl: "http://bucket.s3.com/my-template.yaml",
+ entity: mockedTemplateEntity,
+ entityRef: stringifyEntityRef(mockedTemplateEntity),
+ },
+ input: mockTemplateCatalogCreateInput,
+ workspacePath: workspacePath,
+ });
+
+ await (
+ await createAcdpCatalogCreateAction({
+ config: mockConfig,
+ reader: mockUrlReader,
+ integrations: mockIntegrations,
+ catalogClient: catalogClient,
+ discovery: mockDiscovery,
+ tokenManager: mockTokenManager,
+ logger: getVoidLogger(),
+ })
+ ).handler(mockContext);
+
+ expect(catalogClient.getEntityByRef.mock.calls.length === 2);
+ expect(fetchContents).toHaveBeenCalledTimes(2);
+ expect(mockedS3Client.calls()).toHaveLength(1);
+ expect(mockContext.output).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-catalog-create.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-catalog-create.ts
new file mode 100644
index 00000000..6579d962
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-catalog-create.ts
@@ -0,0 +1,420 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Config } from "@backstage/config";
+import { JsonObject } from "@backstage/types";
+import {
+ createTemplateAction,
+ fetchContents,
+ ActionContext,
+} from "@backstage/plugin-scaffolder-node";
+import {
+ UrlReader,
+ resolveSafeChildPath,
+ PluginEndpointDiscovery,
+ TokenManager,
+} from "@backstage/backend-common";
+import { DefaultAwsCredentialsManager } from "@backstage/integration-aws-node";
+import { CatalogClient, Location } from "@backstage/catalog-client";
+import {
+ CompoundEntityRef,
+ DEFAULT_NAMESPACE,
+ ANNOTATION_SOURCE_LOCATION,
+ Entity,
+ parseLocationRef,
+} from "@backstage/catalog-model";
+import { Publisher, PublisherBase } from "@backstage/plugin-techdocs-node";
+import { ScmIntegrations } from "@backstage/integration";
+import { InputError } from "@backstage/errors";
+import { AwsS3Helper } from "../utils/aws-s3-helper";
+import * as path from "path";
+import { getLocationForEntity } from "../utils/location-helper";
+
+import * as yaml from "yaml";
+import { z } from "zod";
+
+import {
+ PutObjectCommand,
+ PutObjectCommandInput,
+ S3Client,
+} from "@aws-sdk/client-s3";
+import { constants } from "backstage-plugin-acdp-common";
+import { Logger } from "winston";
+
+interface CatalogConfig {
+ bucketName: string;
+ region: string;
+ catalogPrefix: string;
+ catalogItemAssetsPath: string;
+ allowUnsafeAccess: boolean;
+}
+
+interface CtxInput extends JsonObject {
+ componentId: string;
+ assetsSourcePath?: string;
+ docsSiteSourcePath?: string;
+ entity: any;
+}
+
+interface AcdpCatalogCreateActionInput {
+ config: Config;
+ reader: UrlReader;
+ integrations: ScmIntegrations;
+ catalogClient: CatalogClient;
+ discovery: PluginEndpointDiscovery;
+ tokenManager: TokenManager;
+ logger: Logger;
+}
+
+export const createAcdpCatalogCreateAction = async (
+ options: AcdpCatalogCreateActionInput,
+) => {
+ const { config, catalogClient, discovery, logger, tokenManager } = options;
+
+ const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config);
+ const credentialProvider =
+ await awsCredentialsManager.getCredentialProvider();
+ const userAgentString = config.getString("acdp.metrics.userAgentString");
+
+ const catalogConfig: CatalogConfig = {
+ bucketName: config.getString("acdp.s3Catalog.bucketName"),
+ region: config.getString("acdp.s3Catalog.region"),
+ catalogPrefix: config.getString("acdp.s3Catalog.prefix"),
+ catalogItemAssetsPath:
+ config.getOptionalString("acdp.s3Catalog.catalogItemAssetsPath") ??
+ "assets/",
+ allowUnsafeAccess:
+ config.getOptionalBoolean("acdp.allow-unsafe-local-dir-access") ?? false,
+ };
+
+ if (!catalogConfig.catalogItemAssetsPath.endsWith("/")) {
+ logger.error(
+ "acdp.s3Catalog.catalogItemAssetsPath must have a trailing slash",
+ );
+ throw new Error("Invalid acdp.s3Catalog.catalogItemAssetsPath");
+ }
+
+ const techdocsPublisher = await Publisher.fromConfig(config, {
+ logger: logger,
+ discovery: discovery,
+ });
+ await techdocsPublisher.getReadiness();
+
+ return createTemplateAction({
+ id: "aws:acdp:catalog:create",
+ description:
+ "Writes the catalog-info.yaml and copies assets for your template to the backend s3 bucket",
+ schema: {
+ input: z.object({
+ componentId: z
+ .string()
+ .describe(
+ "The unique component id which is used for the catalog-info name",
+ ),
+ assetsSourcePath: z
+ .string()
+ .optional()
+ .describe(
+ "optional: path to the assets used by this component to copy into the catalog item's assets folder",
+ ),
+ docsSiteSourcePath: z
+ .string()
+ .optional()
+ .describe(
+ "optional: path to the techdocs site folder to copy into the techdocs' assets store. Techdocs must be configured for this to work.",
+ ),
+ entity: z
+ .record(z.any())
+ .describe(
+ "YAML body for the catalog-info.yaml content. It will automatically be updated with ACDP Metadata",
+ ),
+ }),
+ output: {
+ type: "object",
+ properties: {
+ s3Url: {
+ title: "S3 URL Path file was upload to",
+ type: "string",
+ },
+ s3Uri: {
+ title: "S3 URI Path file was upload to",
+ type: "string",
+ },
+ },
+ },
+ },
+
+ async handler(ctx) {
+ const { token } = await tokenManager.getToken();
+
+ if (
+ ctx.templateInfo === undefined ||
+ ctx.templateInfo.baseUrl === undefined
+ ) {
+ throw new InputError("Unable to read template info");
+ }
+
+ const compoundEntity: CompoundEntityRef = {
+ kind: ctx.input.entity.kind.toLowerCase(),
+ namespace: (
+ ctx.input.entity.metadata?.namespace ?? DEFAULT_NAMESPACE
+ ).toLowerCase(),
+ name: ctx.input.entity.metadata.name.toLowerCase(),
+ };
+ const entity = {
+ kind: compoundEntity.kind,
+ metadata: {
+ namespace: compoundEntity.namespace,
+ name: compoundEntity.name,
+ },
+ } as Entity;
+
+ // Check if a registered entity already exists
+ const existingEntity = await catalogClient.getEntityByRef(
+ compoundEntity,
+ {
+ token: token,
+ },
+ );
+ if (existingEntity)
+ throw new Error(
+ `An entity the ref ${existingEntity.metadata.namespace}/${existingEntity.kind}/${existingEntity.metadata.name} already exists`,
+ );
+
+ const catalogEntityPathPrefix = `${catalogConfig.catalogPrefix}/${compoundEntity.namespace}/${compoundEntity.kind}/${compoundEntity.name}`;
+
+ const s3Client = new S3Client({
+ region: catalogConfig.region,
+ customUserAgent: userAgentString,
+ credentialDefaultProvider: () =>
+ credentialProvider.sdkCredentialProvider,
+ });
+
+ if (ctx.input.docsSiteSourcePath != undefined) {
+ await copyDocsAssetsToCatalog({
+ techdocsPublisher: techdocsPublisher,
+ catalogConfig: catalogConfig,
+ catalogCreateInput: options,
+ ctx: ctx,
+ entity: entity,
+ });
+ } else {
+ ctx.logger.info(
+ "Skipping techdocs upload...docsSiteSourcePath is unset",
+ );
+ }
+
+ if (ctx.input.assetsSourcePath != undefined) {
+ await copyAssetsToCatalog({
+ s3Client: s3Client,
+ catalogConfig: catalogConfig,
+ catalogCreateInput: options,
+ ctx: ctx,
+ catalogEntityPathPrefix: catalogEntityPathPrefix,
+ entity: entity,
+ });
+ } else {
+ ctx.logger.info("Skipping assets upload...assetsSourcePath is unset");
+ }
+
+ await writeCatalogItemToS3({
+ s3Client: s3Client,
+ catalogConfig: catalogConfig,
+ ctx: ctx,
+ catalogEntityPathPrefix: catalogEntityPathPrefix,
+ });
+ },
+ });
+};
+
+const copyDocsAssetsToCatalog = async (options: {
+ techdocsPublisher: PublisherBase;
+ catalogConfig: CatalogConfig;
+ catalogCreateInput: AcdpCatalogCreateActionInput;
+ ctx: ActionContext;
+ entity: Entity;
+}) => {
+ const { techdocsPublisher, catalogConfig, catalogCreateInput, ctx, entity } =
+ options;
+
+ const docsTargetPath = "./techdocs";
+ const docsTmpPath = resolveSafeChildPath(ctx.workspacePath, docsTargetPath);
+
+ const templateBaseUrl = ctx.templateInfo!.baseUrl!;
+ let fetchBaseUrl = templateBaseUrl;
+ if (
+ catalogConfig.allowUnsafeAccess &&
+ templateBaseUrl.startsWith("file://")
+ ) {
+ fetchBaseUrl = "file:///"; //allow access to full local filesystem for local development
+ }
+
+ ctx.logger.info("Starting: Fetching docs from source location");
+
+ const location = parseLocationRef(ctx.input.docsSiteSourcePath!) as Location;
+
+ const resolvedLocation = getLocationForEntity(
+ location,
+ templateBaseUrl,
+ catalogCreateInput.integrations,
+ catalogConfig.allowUnsafeAccess,
+ );
+
+ await fetchContents({
+ reader: catalogCreateInput.reader,
+ integrations: catalogCreateInput.integrations,
+ baseUrl: fetchBaseUrl,
+ fetchUrl: resolvedLocation.target,
+ outputPath: docsTmpPath,
+ // token: ctx.input.token, #This is added in next patch version of the fetchContents func...uncomment this on next lib bump
+ });
+ ctx.logger.info("Finished: Fetching docs from source location");
+ ctx.logger.info("Starting: Publishing docs to techdocs asset location");
+ await techdocsPublisher.publish({
+ entity: entity,
+ directory: docsTmpPath,
+ });
+ ctx.logger.info("Finished: Publishing docs to techdocs asset location");
+};
+
+const copyAssetsToCatalog = async (options: {
+ s3Client: S3Client;
+ catalogConfig: CatalogConfig;
+ catalogCreateInput: AcdpCatalogCreateActionInput;
+ ctx: ActionContext;
+ catalogEntityPathPrefix: string;
+ entity: Entity;
+}) => {
+ const {
+ s3Client,
+ catalogConfig,
+ catalogCreateInput,
+ ctx,
+ catalogEntityPathPrefix,
+ entity,
+ } = options;
+
+ const s3Helper = new AwsS3Helper({
+ s3Client: s3Client,
+ bucketName: catalogConfig.bucketName,
+ logger: ctx.logger,
+ });
+
+ const assetsTargetPath = "./assets";
+
+ const assetsTmpPath = resolveSafeChildPath(
+ ctx.workspacePath,
+ assetsTargetPath,
+ );
+
+ const templateBaseUrl = ctx.templateInfo!.baseUrl!;
+
+ let fetchBaseUrl = templateBaseUrl;
+ if (
+ catalogConfig.allowUnsafeAccess &&
+ templateBaseUrl.startsWith("file://")
+ ) {
+ fetchBaseUrl = "file:///"; //allow access to full local filesystem for local development
+ }
+
+ const location = parseLocationRef(ctx.input.assetsSourcePath!) as Location;
+ const resolvedLocation = getLocationForEntity(
+ location,
+ templateBaseUrl,
+ catalogCreateInput.integrations,
+ catalogConfig.allowUnsafeAccess,
+ );
+ await fetchContents({
+ reader: catalogCreateInput.reader,
+ integrations: catalogCreateInput.integrations,
+ baseUrl: fetchBaseUrl,
+ fetchUrl: resolvedLocation.target,
+ outputPath: assetsTmpPath,
+ // token: ctx.input.token, #This is added in next patch version of the fetchContents func...uncomment this on next lib bump
+ });
+
+ const assetsUploadPathPrefix = path.join(
+ catalogEntityPathPrefix,
+ catalogConfig.catalogItemAssetsPath,
+ );
+
+ ctx.logger.debug(`Uploading assets to: ${assetsUploadPathPrefix}`);
+ ctx.logger.info(`Finding and deleting existing assets from catalog`);
+ const existingCatalogObjects = await s3Helper.getAllObjectsFromBucket(
+ assetsUploadPathPrefix,
+ );
+ s3Helper.deleteObjectsFromBucket(existingCatalogObjects);
+
+ ctx.logger.info(`Uploading assets to catalog`);
+ s3Helper.uploadFilesToBucket(entity, assetsTmpPath, assetsUploadPathPrefix);
+};
+
+const writeCatalogItemToS3 = async (options: {
+ s3Client: S3Client;
+ catalogConfig: CatalogConfig;
+ catalogEntityPathPrefix: string;
+ ctx: ActionContext;
+}) => {
+ const {
+ s3Client,
+ catalogConfig,
+ catalogEntityPathPrefix: catalogEntityS3KeyPrefix,
+ ctx,
+ } = options;
+
+ const catalogInfoS3Key = `${catalogEntityS3KeyPrefix}/catalog-info.yaml`;
+ const assetsS3Key = `${catalogEntityS3KeyPrefix}/${catalogConfig.catalogItemAssetsPath}`;
+ const catalogAssetsPathUrl = `https://${catalogConfig.bucketName}.s3.${catalogConfig.region}.amazonaws.com/${assetsS3Key}`;
+ const catalogInfoUrl = `https://${catalogConfig.bucketName}.s3.${catalogConfig.region}.amazonaws.com/${catalogInfoS3Key}`;
+ ctx.logger.info(
+ `Writing catalog-info to ${catalogConfig.bucketName}/${catalogInfoS3Key}`,
+ );
+
+ // Inject required annotations into catalog-info.yaml
+
+ //For now, we only support 1 deployment target. in the future, this should come from inputs
+ ctx.input.entity.metadata.annotations[
+ constants.ACDP_DEPLOYMENT_TARGET_ANNOTATION
+ ] = constants.ACDP_DEFAULT_DEPLOYMENT_TARGET;
+
+ if (ctx.input.docsSiteSourcePath != undefined) {
+ ctx.input.entity.metadata.annotations["aws.amazon.com/techdocs-builder"] =
+ "external";
+ ctx.input.entity.metadata.annotations["backstage.io/techdocs-ref"] =
+ "dir:.";
+ }
+
+ ctx.input.entity.metadata.annotations["aws.amazon.com/template-entity-ref"] =
+ ctx.templateInfo?.entityRef;
+
+ ctx.input.entity.metadata.annotations[constants.ACDP_ASSETS_REF] =
+ `dir:${path.join(".", catalogConfig.catalogItemAssetsPath)}`;
+
+ ctx.input.entity.metadata.annotations[constants.ACDP_ASSETS_STORED] = (
+ ctx.input.assetsSourcePath != undefined
+ ).toString();
+
+ ctx.input.entity.metadata.annotations[ANNOTATION_SOURCE_LOCATION] =
+ `url:${catalogAssetsPathUrl}`;
+
+ const putCatalogEntityInput: PutObjectCommandInput = {
+ Body: yaml.stringify(ctx.input.entity),
+ Bucket: catalogConfig.bucketName,
+ Key: catalogInfoS3Key,
+ };
+ const putCatalogEntityResp = await s3Client.send(
+ new PutObjectCommand(putCatalogEntityInput),
+ );
+
+ if (putCatalogEntityResp.ETag !== undefined) {
+ ctx.logger.info("Successfully created catalog item");
+ ctx.logger.debug(
+ `Successfully created s3 object for catalog item: s3://${putCatalogEntityInput.Bucket}/${putCatalogEntityInput.Key}`,
+ );
+ ctx.output("catalogItemS3Url", catalogInfoUrl);
+ ctx.output(
+ "catalogItemS3Uri",
+ `s3://${catalogConfig.bucketName}/${catalogInfoS3Key}`,
+ );
+ }
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-configure.test.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-configure.test.ts
new file mode 100644
index 00000000..d5299185
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-configure.test.ts
@@ -0,0 +1,60 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { getVoidLogger } from "@backstage/backend-common";
+import { mockClient } from "aws-sdk-client-mock";
+import { CodeBuild, StartBuildCommand } from "@aws-sdk/client-codebuild";
+
+import { createAcdpConfigureAction } from ".";
+import {
+ mockCatalogClient,
+ mockUrlReader,
+ mockConfig,
+ mockedCatalogEntity,
+ mockIntegrations,
+ mockTokenManager,
+} from "../__mocks__/common-mocks";
+import { stringifyEntityRef } from "@backstage/catalog-model";
+import { createMockDirectory } from "@backstage/backend-test-utils";
+import { createMockActionContext } from "@backstage/plugin-scaffolder-node-test-utils";
+
+jest.mock("../service/acdp-build-service");
+const mockedCodeBuildClient = mockClient(CodeBuild);
+
+beforeEach(() => {
+ mockedCodeBuildClient.reset();
+});
+
+describe("createAcdpConfigureAction", () => {
+ const workspacePath = createMockDirectory().resolve("/tmp");
+
+ it("", async () => {
+ const mockContext = createMockActionContext({
+ templateInfo: {
+ entityRef: stringifyEntityRef(mockedCatalogEntity),
+ },
+ input: {
+ entityRef: stringifyEntityRef(mockedCatalogEntity),
+ buildParameters: [{ name: "test", value: "test" }],
+ },
+ workspacePath: workspacePath,
+ });
+
+ mockedCodeBuildClient.on(StartBuildCommand).resolves({
+ build: {
+ arn: "arn:aws:codebuild:us-west-2:111111111111:build/test:test",
+ id: "test",
+ },
+ });
+ await (
+ await createAcdpConfigureAction({
+ config: mockConfig,
+ reader: mockUrlReader,
+ integrations: mockIntegrations,
+ catalogClient: mockCatalogClient(mockedCatalogEntity),
+ tokenManager: mockTokenManager,
+ logger: getVoidLogger(),
+ })
+ ).handler(mockContext);
+ });
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-configure.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-configure.ts
new file mode 100644
index 00000000..b26765e0
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/acdp-configure.ts
@@ -0,0 +1,158 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Config } from "@backstage/config";
+import { createTemplateAction } from "@backstage/plugin-scaffolder-node";
+import { z } from "zod";
+import { DefaultAwsCredentialsManager } from "@backstage/integration-aws-node";
+import { AcdpBuildService } from "../service/acdp-build-service";
+import { TokenManager, UrlReader } from "@backstage/backend-common";
+import { CatalogClient } from "@backstage/catalog-client";
+import { ScmIntegrations } from "@backstage/integration";
+import { Logger } from "winston";
+import { AcdpBuildAction, constants } from "backstage-plugin-acdp-common";
+import { Entity, parseEntityRef } from "@backstage/catalog-model";
+import { JsonObject } from "@backstage/types";
+import { SourceType } from "@aws-sdk/client-codebuild";
+
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+interface BuildParameter extends JsonObject {
+ name: string;
+ value: string;
+}
+
+interface SourceOverrideConfig extends JsonObject {
+ sourceType?: SourceType;
+ sourceLocation?: string;
+ sourceVersion?: string;
+}
+
+interface CtxInput extends JsonObject {
+ entityRef: string;
+ buildParameters: BuildParameter[];
+ sourceOverrideConfig?: SourceOverrideConfig;
+}
+
+export const createAcdpConfigureAction = async (options: {
+ config: Config;
+ reader: UrlReader;
+ integrations: ScmIntegrations;
+ catalogClient: CatalogClient;
+ tokenManager: TokenManager;
+ logger: Logger;
+}) => {
+ const { config, reader, integrations, catalogClient, logger, tokenManager } =
+ options;
+
+ const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider =
+ await awsCredentialsManager.getCredentialProvider();
+
+ const acdpBuildService = new AcdpBuildService({
+ config: config,
+ reader: reader,
+ integrations: integrations,
+ awsCredentialsProvider: awsCredentialProvider,
+ logger: logger,
+ });
+
+ return createTemplateAction({
+ id: "aws:acdp:configure",
+ description:
+ "Registers and configures the catalog item to be able to run ACDP builds",
+ schema: {
+ input: z.object({
+ entityRef: z.string(),
+ sourceOverrideConfig: z
+ .object({
+ sourceType: z.enum(["S3", "GITHUB", "CODECOMMIT", "NO_SOURCE"]),
+ sourceLocation: z.string(),
+ sourceVersion: z.string().optional(),
+ })
+ .optional(),
+ buildParameters: z
+ .array(
+ z.object({
+ name: z.string(),
+ value: z.string(),
+ }),
+ )
+ .optional(),
+ }),
+ output: {
+ type: "object",
+ properties: {
+ codeBuildProjectArn: {
+ title: "CodeBuild Project used to deploy",
+ type: "string",
+ },
+ },
+ },
+ },
+
+ async handler(ctx) {
+ const { token } = await tokenManager.getToken();
+ const entityRef = parseEntityRef(ctx.input.entityRef);
+
+ let entity: Entity | undefined = undefined;
+ const maxRetries = 10;
+ let tryCount = 0;
+ do {
+ if (tryCount > 0) await sleep(5000);
+ tryCount++;
+ entity = await catalogClient.getEntityByRef(entityRef, {
+ token: token,
+ });
+ } while (entity === undefined && tryCount < maxRetries);
+
+ if (entity === undefined) {
+ throw new Error(
+ "Failed to configure ACDP build due to entity not yet showing up in the catalog",
+ );
+ }
+
+ const environmentVariables = [
+ {
+ name: "MODULE_STACK_NAME",
+ value: `${entity?.metadata.namespace}-${entity?.metadata.name}`,
+ },
+ ];
+
+ if (ctx.input.buildParameters)
+ environmentVariables.push(...ctx.input.buildParameters);
+
+ await acdpBuildService.storeBuildEnvironmentVariables(
+ entity,
+ environmentVariables,
+ );
+
+ if (!ctx.input.sourceOverrideConfig) {
+ await acdpBuildService.storeBuildSourceConfig(entity, {
+ useEntityAssets:
+ entity.metadata?.annotations?.[constants.ACDP_ASSETS_STORED] ===
+ "true",
+ });
+ } else {
+ await acdpBuildService.storeBuildSourceConfig(entity, {
+ useEntityAssets: false,
+ sourceType: ctx.input.sourceOverrideConfig.sourceType as SourceType,
+ sourceLocation: ctx.input.sourceOverrideConfig.sourceLocation,
+ sourceVersion: ctx.input.sourceOverrideConfig.sourceVersion,
+ });
+ }
+
+ const shouldDeployAnnotation =
+ entity?.metadata?.annotations![
+ constants.ACDP_DEPLOY_ON_CREATE_ANNOTATION
+ ];
+
+ if (shouldDeployAnnotation == "true") {
+ await acdpBuildService.startBuild({
+ entity: entity,
+ action: AcdpBuildAction.DEPLOY,
+ });
+ }
+ },
+ });
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/index.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/index.ts
new file mode 100644
index 00000000..bc426e45
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/index.ts
@@ -0,0 +1,6 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./acdp-configure";
+export * from "./yamlFsWriter";
+export * from "./acdp-catalog-create";
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/yamlFsWriter.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/yamlFsWriter.ts
new file mode 100644
index 00000000..6bea82d1
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/actions/yamlFsWriter.ts
@@ -0,0 +1,39 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { createTemplateAction } from "@backstage/plugin-scaffolder-node";
+import * as yaml from "yaml";
+import { z } from "zod";
+import * as fs from "fs";
+
+export const createNewYamlFileAction = () => {
+ return createTemplateAction({
+ id: "aws:fs:write-yaml",
+ description: "Writes the input as a workspace file",
+ schema: {
+ input: z.object({
+ filename: z.string().describe("The filename to write"),
+ entity: z.record(z.any()).describe("YAML body for the file content"),
+ }),
+ output: {
+ type: "object",
+ properties: {
+ filePath: {
+ title: "Workspace path file was written to",
+ type: "string",
+ },
+ },
+ },
+ },
+
+ async handler(ctx) {
+ const filepath = `${ctx.workspacePath}/${ctx.input.filename}`;
+
+ fs.writeFileSync(filepath, yaml.stringify(ctx.input.entity));
+
+ ctx.logger.info(`Successfully created file: ${ctx.input.filename}`);
+
+ ctx.output("filename", ctx.input.filename);
+ },
+ });
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/api/acdp-build-api.test.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/api/acdp-build-api.test.ts
new file mode 100644
index 00000000..dd0c7fca
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/api/acdp-build-api.test.ts
@@ -0,0 +1,206 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { mockClient } from "aws-sdk-client-mock";
+import { AcdpBuildApi } from ".";
+import {
+ BatchGetProjectsCommand,
+ CodeBuildClient,
+ ListBuildsForProjectCommand,
+ BatchGetBuildsCommand,
+ StartBuildCommand,
+} from "@aws-sdk/client-codebuild";
+import {
+ mockUrlReader,
+ mockedConfigData,
+ mockedCatalogEntity,
+ resetMocks,
+ mockCatalogClient,
+} from "../__mocks__/common-mocks";
+import { AcdpBuildAction, constants } from "backstage-plugin-acdp-common";
+import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
+import { MockedAcdpBuildService } from "../service/__mocks__/acdp-build-service.mock";
+
+const mockedCodeBuildClient = mockClient(CodeBuildClient);
+const mockedSsmClient = mockClient(SSMClient);
+let mockedAcdpBuildService: MockedAcdpBuildService;
+let acdpBuildApi: AcdpBuildApi;
+beforeAll(async () => {
+ mockedAcdpBuildService = new MockedAcdpBuildService();
+ acdpBuildApi = new AcdpBuildApi(
+ mockCatalogClient(mockedCatalogEntity),
+ mockedAcdpBuildService,
+ );
+});
+
+beforeEach(() => {
+ mockedCodeBuildClient.reset();
+ mockedSsmClient.reset();
+ resetMocks();
+});
+
+function setupCommonBuildMocks() {
+ mockedCodeBuildClient.on(StartBuildCommand).resolves({});
+
+ mockedSsmClient
+ .on(GetParameterCommand, {
+ Name: mockedAcdpBuildService.getSsmParameterNameForEntityBuildParameters(
+ mockedCatalogEntity,
+ ),
+ })
+ .resolves({
+ Parameter: {
+ Value: JSON.stringify([
+ { name: "MODULE_STACK_NAME", value: "acdp-cms-sample" },
+ {
+ name: "CFN_TEMPLATE_URL",
+ value:
+ "https://acdp-assets.s3.us-west-2.amazonaws.com/connected-mobility-solution-on-aws/v1.1.0/cms-sample/cms-sample.template",
+ },
+ { name: "APP_UNIQUE_ID", value: "cms" },
+ ]),
+ },
+ });
+
+ mockedSsmClient
+ .on(GetParameterCommand, {
+ Name: mockedAcdpBuildService.getSsmParameterNameForEntitySourceConfig(
+ mockedCatalogEntity,
+ ),
+ })
+ .resolves({
+ Parameter: {
+ Value: JSON.stringify({
+ useEntityAssets: true,
+ }),
+ },
+ });
+}
+
+describe("AcdpBuildApi", () => {
+ describe("getProject", () => {
+ it("should return project", async () => {
+ mockedCodeBuildClient.on(BatchGetProjectsCommand).resolves({
+ projects: [
+ {
+ arn: mockedConfigData.acdp.deploymentDefaults.codeBuildProjectArn,
+ },
+ ],
+ });
+
+ const project = await acdpBuildApi.getProject(mockedCatalogEntity);
+
+ expect(mockedCodeBuildClient.calls()).toHaveLength(1);
+ expect(project?.arn).toEqual(
+ mockedConfigData.acdp.deploymentDefaults.codeBuildProjectArn,
+ );
+ });
+ });
+
+ describe("getBuilds", () => {
+ it("should return builds filtered by BACKSTAGE_ENTITY_UID", async () => {
+ const backstageEntityUid = mockedCatalogEntity.metadata.uid;
+ mockedCodeBuildClient.on(ListBuildsForProjectCommand).resolves({
+ ids: ["test-build-id-1", "test-build-id-2"],
+ });
+ mockedCodeBuildClient.on(BatchGetBuildsCommand).resolves({
+ builds: [
+ {
+ buildNumber: 1,
+ buildComplete: true,
+ buildStatus: "SUCCEEDED",
+ arn: "arn:aws:codebuild:us-west-2:111111111111:build/test:test",
+ endTime: new Date("2023-01-01T23:34:38.397Z"),
+ startTime: new Date("2023-01-01T23:31:26.086Z"),
+ environment: {
+ environmentVariables: [
+ {
+ name: constants.BACKSTAGE_ENTITY_UID_ENVIRONMENT_VARIABLE,
+ value: backstageEntityUid,
+ },
+ ],
+ computeType: "BUILD_GENERAL1_SMALL",
+ image: "aws/codebuild/standard:5.0",
+ type: "LINUX_CONTAINER",
+ },
+ },
+ {
+ buildNumber: 2,
+ buildComplete: true,
+ buildStatus: "SUCCEEDED",
+ arn: "arn:aws:codebuild:us-west-2:111111111111:build/test:test",
+ endTime: new Date("2023-01-01T23:34:38.397Z"),
+ startTime: new Date("2023-01-01T23:31:26.086Z"),
+ environment: {
+ environmentVariables: [
+ {
+ name: "MODULE_STACK_NAME",
+ value: "other-stack",
+ },
+ ],
+ computeType: "BUILD_GENERAL1_SMALL",
+ image: "aws/codebuild/standard:5.0",
+ type: "LINUX_CONTAINER",
+ },
+ },
+ ],
+ });
+
+ const builds = await acdpBuildApi.getBuilds(mockedCatalogEntity);
+
+ expect(mockedCodeBuildClient.calls()).toHaveLength(2);
+ expect(builds).toHaveLength(1);
+ });
+
+ it("should return empty list for no builds", async () => {
+ mockedCodeBuildClient.on(ListBuildsForProjectCommand).resolves({});
+
+ const builds = await acdpBuildApi.getBuilds(mockedCatalogEntity);
+
+ expect(mockedCodeBuildClient.calls()).toHaveLength(1);
+ expect(builds).toEqual([]);
+ });
+ });
+
+ describe("startDeployBuild", () => {
+ it("should startDeployBuild", async () => {
+ setupCommonBuildMocks();
+
+ await acdpBuildApi.startBuild(
+ mockedCatalogEntity,
+ AcdpBuildAction.DEPLOY,
+ );
+
+ expect(mockUrlReader.readUrl.mock.calls).toHaveLength(1);
+ expect(mockedCodeBuildClient.calls()).toHaveLength(1);
+ });
+ });
+
+ describe("startUpdateBuild", () => {
+ it("should startUpdateBuild", async () => {
+ setupCommonBuildMocks();
+
+ await acdpBuildApi.startBuild(
+ mockedCatalogEntity,
+ AcdpBuildAction.UPDATE,
+ );
+
+ expect(mockUrlReader.readUrl.mock.calls).toHaveLength(1);
+ expect(mockedCodeBuildClient.calls()).toHaveLength(1);
+ });
+ });
+
+ describe("startTeardownBuild", () => {
+ it("should startTeardownBuild", async () => {
+ setupCommonBuildMocks();
+
+ await acdpBuildApi.startBuild(
+ mockedCatalogEntity,
+ AcdpBuildAction.DEPLOY,
+ );
+
+ expect(mockUrlReader.readUrl.mock.calls).toHaveLength(1);
+ expect(mockedCodeBuildClient.calls()).toHaveLength(1);
+ });
+ });
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/api/acdp-build-api.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/api/acdp-build-api.ts
new file mode 100644
index 00000000..a120e9ff
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/api/acdp-build-api.ts
@@ -0,0 +1,93 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { AcdpBuildService } from "../service/acdp-build-service";
+import { CatalogClient } from "@backstage/catalog-client";
+import { Entity } from "@backstage/catalog-model";
+import { NotFoundError } from "@backstage/errors";
+import {
+ AcdpBuildAction,
+ AcdpBuildProject,
+ AcdpBuildProjectBuild,
+} from "backstage-plugin-acdp-common";
+
+export class AcdpBuildApi {
+ private catalogClient: CatalogClient;
+ private acdpBuildService: AcdpBuildService;
+
+ public constructor(
+ catalogClient: CatalogClient,
+ acdpBuildService: AcdpBuildService,
+ ) {
+ this.catalogClient = catalogClient;
+ this.acdpBuildService = acdpBuildService;
+ }
+
+ public async getProject(
+ entity: Entity,
+ ): Promise {
+ const codeBuildProject = await this.acdpBuildService.getProject(entity);
+
+ if (codeBuildProject === undefined) return undefined;
+
+ return {
+ name: codeBuildProject.name,
+ arn: codeBuildProject.arn,
+ };
+ }
+
+ public async getBuilds(entity: Entity): Promise {
+ const codeBuildProjectBuilds =
+ await this.acdpBuildService.getBuilds(entity);
+
+ return codeBuildProjectBuilds.map((build) => {
+ return {
+ id: build.id,
+ arn: build.arn,
+ buildNumber: build.buildNumber,
+ startTime: build.startTime,
+ endTime: build.endTime,
+ currentPhase: build.currentPhase,
+ buildStatus: build.buildStatus,
+ projectName: build.projectName,
+ };
+ });
+ }
+
+ public async startBuild(
+ entity: Entity,
+ action: AcdpBuildAction,
+ ): Promise {
+ const startBuildResponse = await this.acdpBuildService.startBuild({
+ entity: entity,
+ action: action,
+ });
+
+ const build = startBuildResponse.build;
+
+ if (build === undefined) return {};
+
+ return {
+ id: build.id,
+ arn: build.arn,
+ buildNumber: build.buildNumber,
+ startTime: build.startTime,
+ endTime: build.endTime,
+ projectName: build.currentPhase,
+ };
+ }
+
+ public async getEntity(
+ entityRef: string,
+ backstageApiToken: string | undefined,
+ ): Promise {
+ const entity = await this.catalogClient.getEntityByRef(entityRef, {
+ token: backstageApiToken,
+ });
+
+ if (entity === undefined)
+ throw new NotFoundError(`Could not find Entity for ref: '${entityRef}'`);
+
+ return entity;
+ }
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/api/index.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/api/index.ts
new file mode 100644
index 00000000..7074a399
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/api/index.ts
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./acdp-build-api";
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/index.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/index.ts
new file mode 100644
index 00000000..864c643d
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/index.ts
@@ -0,0 +1,7 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./service/router";
+export * from "./api/acdp-build-api";
+export * from "./actions";
+export * from "./service/acdp-build-service";
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/run.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/run.ts
new file mode 100644
index 00000000..adb8351f
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/run.ts
@@ -0,0 +1,20 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { getRootLogger } from "@backstage/backend-common";
+import yn from "yn";
+import { startStandaloneServer } from "./service/standaloneServer";
+
+const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007;
+const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
+const logger = getRootLogger();
+
+startStandaloneServer({ port, enableCors, logger }).catch((err) => {
+ logger.error(err);
+ process.exit(1);
+});
+
+process.on("SIGINT", () => {
+ logger.info("CTRL+C pressed; exiting.");
+ process.exit(0);
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/service/__mocks__/acdp-build-service.mock.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/__mocks__/acdp-build-service.mock.ts
new file mode 100644
index 00000000..51118055
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/__mocks__/acdp-build-service.mock.ts
@@ -0,0 +1,42 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Config } from "@backstage/config";
+import { AcdpBuildService } from "../acdp-build-service";
+import { UrlReader, getVoidLogger } from "@backstage/backend-common";
+import { ScmIntegrations } from "@backstage/integration";
+import { AwsCredentialProvider } from "@backstage/integration-aws-node";
+import { Logger } from "winston";
+import {
+ mockConfig,
+ mockCredentialsProvider,
+ mockIntegrations,
+ mockUrlReader,
+} from "../../__mocks__/common-mocks";
+import { Entity } from "@backstage/catalog-model";
+
+export class MockedAcdpBuildService extends AcdpBuildService {
+ public constructor(
+ config?: Config,
+ reader?: UrlReader,
+ integrations?: ScmIntegrations,
+ awsCredentialsProvider?: AwsCredentialProvider,
+ logger?: Logger,
+ ) {
+ super({
+ config: config ?? mockConfig,
+ reader: reader ?? mockUrlReader,
+ integrations: integrations ?? mockIntegrations,
+ awsCredentialsProvider: awsCredentialsProvider ?? mockCredentialsProvider,
+ logger: logger ?? getVoidLogger(),
+ });
+ }
+
+ public getSsmParameterNameForEntityBuildParameters(entity: Entity) {
+ return super.getSsmParameterNameForEntityBuildParameters(entity);
+ }
+
+ public getSsmParameterNameForEntitySourceConfig(entity: Entity) {
+ return super.getSsmParameterNameForEntitySourceConfig(entity);
+ }
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/service/acdp-build-service.test.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/acdp-build-service.test.ts
new file mode 100644
index 00000000..7e8403c6
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/acdp-build-service.test.ts
@@ -0,0 +1,209 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { mockClient } from "aws-sdk-client-mock";
+import {
+ mockedCatalogEntity,
+ mockedConfigData,
+ resetMocks,
+} from "../__mocks__/common-mocks";
+import {
+ BatchGetBuildsCommand,
+ BatchGetProjectsCommand,
+ CodeBuildClient,
+ ListBuildsForProjectCommand,
+ StartBuildCommand,
+} from "@aws-sdk/client-codebuild";
+import {
+ GetParameterCommand,
+ PutParameterCommand,
+ SSMClient,
+} from "@aws-sdk/client-ssm";
+import { AcdpBuildAction, constants } from "backstage-plugin-acdp-common";
+import { MockedAcdpBuildService } from "./__mocks__/acdp-build-service.mock";
+
+const mockedCodeBuildClient = mockClient(CodeBuildClient);
+const mockedSsmClient = mockClient(SSMClient);
+
+let acdpBuildService: MockedAcdpBuildService;
+beforeAll(async () => {
+ acdpBuildService = new MockedAcdpBuildService();
+});
+
+beforeEach(() => {
+ mockedCodeBuildClient.reset();
+ mockedSsmClient.reset();
+ resetMocks();
+});
+
+function setupCommonBuildMocks() {
+ mockedCodeBuildClient.on(StartBuildCommand).resolves({
+ build: {
+ arn: "arn:aws:codebuild:us-west-2:111111111111:build/test:test",
+ id: "test",
+ },
+ });
+
+ mockedSsmClient.on(PutParameterCommand).resolves({
+ Version: 1,
+ });
+
+ mockedSsmClient
+ .on(GetParameterCommand, {
+ Name: acdpBuildService.getSsmParameterNameForEntityBuildParameters(
+ mockedCatalogEntity,
+ ),
+ })
+ .resolves({
+ Parameter: {
+ Value: JSON.stringify([
+ { name: "MODULE_STACK_NAME", value: "acdp-cms-sample" },
+ {
+ name: "CFN_TEMPLATE_URL",
+ value:
+ "https://acdp-assets.s3.us-west-2.amazonaws.com/connected-mobility-solution-on-aws/v1.1.0/cms-sample/cms-sample.template",
+ },
+ { name: "APP_UNIQUE_ID", value: "cms" },
+ ]),
+ },
+ });
+
+ mockedSsmClient
+ .on(GetParameterCommand, {
+ Name: acdpBuildService.getSsmParameterNameForEntitySourceConfig(
+ mockedCatalogEntity,
+ ),
+ })
+ .resolves({
+ Parameter: {
+ Value: JSON.stringify({
+ useEntityAssets: true,
+ }),
+ },
+ });
+}
+
+describe("getProject", () => {
+ it("should return project", async () => {
+ mockedCodeBuildClient.on(BatchGetProjectsCommand).resolves({
+ projects: [
+ {
+ arn: mockedConfigData.acdp.deploymentDefaults.codeBuildProjectArn,
+ },
+ ],
+ });
+
+ const project = await acdpBuildService.getProject(mockedCatalogEntity);
+
+ expect(mockedCodeBuildClient.calls()).toHaveLength(1);
+ expect(project?.arn).toEqual(
+ mockedConfigData.acdp.deploymentDefaults.codeBuildProjectArn,
+ );
+ });
+});
+
+describe("getBuilds", () => {
+ it("should return builds filtered by BACKSTAGE_ENTITY_UID", async () => {
+ const backstageEntityUid = mockedCatalogEntity.metadata.uid;
+ mockedCodeBuildClient.on(ListBuildsForProjectCommand).resolves({
+ ids: ["test-build-id-1", "test-build-id-2"],
+ });
+ mockedCodeBuildClient.on(BatchGetBuildsCommand).resolves({
+ builds: [
+ {
+ buildNumber: 1,
+ buildComplete: true,
+ buildStatus: "SUCCEEDED",
+ arn: "arn:aws:codebuild:us-west-2:111111111111:build/test:test",
+ endTime: new Date("2023-01-01T23:34:38.397Z"),
+ startTime: new Date("2023-01-01T23:31:26.086Z"),
+ environment: {
+ environmentVariables: [
+ {
+ name: constants.BACKSTAGE_ENTITY_UID_ENVIRONMENT_VARIABLE,
+ value: backstageEntityUid,
+ },
+ ],
+ computeType: "BUILD_GENERAL1_SMALL",
+ image: "aws/codebuild/standard:5.0",
+ type: "LINUX_CONTAINER",
+ },
+ },
+ {
+ buildNumber: 2,
+ buildComplete: true,
+ buildStatus: "SUCCEEDED",
+ arn: "arn:aws:codebuild:us-west-2:111111111111:build/test:test",
+ endTime: new Date("2023-01-01T23:34:38.397Z"),
+ startTime: new Date("2023-01-01T23:31:26.086Z"),
+ environment: {
+ environmentVariables: [
+ {
+ name: "MODULE_STACK_NAME",
+ value: "other-stack",
+ },
+ ],
+ computeType: "BUILD_GENERAL1_SMALL",
+ image: "aws/codebuild/standard:5.0",
+ type: "LINUX_CONTAINER",
+ },
+ },
+ ],
+ });
+
+ const builds = await acdpBuildService.getBuilds(mockedCatalogEntity);
+
+ expect(mockedCodeBuildClient.calls()).toHaveLength(2);
+ expect(builds).toHaveLength(1);
+ });
+ it("should return empty list for no builds", async () => {
+ mockedCodeBuildClient.on(ListBuildsForProjectCommand).resolves({});
+
+ const builds = await acdpBuildService.getBuilds(mockedCatalogEntity);
+
+ expect(mockedCodeBuildClient.calls()).toHaveLength(1);
+ expect(builds).toEqual([]);
+ });
+});
+
+describe("startDeployBuild", () => {
+ it("should startDeployBuild", async () => {
+ setupCommonBuildMocks();
+
+ await acdpBuildService.startBuild({
+ entity: mockedCatalogEntity,
+ action: AcdpBuildAction.DEPLOY,
+ });
+
+ expect(mockedSsmClient.calls()).toHaveLength(2);
+ expect(mockedCodeBuildClient.calls()).toHaveLength(1);
+ });
+});
+
+describe("startUpdateBuild", () => {
+ it("should startUpdateBuild", async () => {
+ setupCommonBuildMocks();
+
+ await acdpBuildService.startBuild({
+ entity: mockedCatalogEntity,
+ action: AcdpBuildAction.UPDATE,
+ });
+
+ expect(mockedSsmClient.calls()).toHaveLength(2);
+ expect(mockedCodeBuildClient.calls()).toHaveLength(1);
+ });
+});
+
+describe("startTeardownBuild", () => {
+ it("should startTeardownBuild", async () => {
+ setupCommonBuildMocks();
+
+ await acdpBuildService.startBuild({
+ entity: mockedCatalogEntity,
+ action: AcdpBuildAction.TEARDOWN,
+ });
+
+ expect(mockedSsmClient.calls()).toHaveLength(2);
+ expect(mockedCodeBuildClient.calls()).toHaveLength(1);
+ });
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/service/acdp-build-service.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/acdp-build-service.ts
new file mode 100644
index 00000000..f4b7a06a
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/acdp-build-service.ts
@@ -0,0 +1,511 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Config } from "@backstage/config";
+import { parse } from "@aws-sdk/util-arn-parser";
+import {
+ BatchGetBuildsCommand,
+ BatchGetProjectsCommand,
+ Build,
+ CodeBuildClient,
+ EnvironmentVariable,
+ ListBuildsForProjectCommand,
+ Project,
+ SourceType,
+ StartBuildCommand,
+ StartBuildCommandOutput,
+} from "@aws-sdk/client-codebuild";
+
+import { AwsCredentialProvider } from "@backstage/integration-aws-node";
+
+import { getLocationForEntity } from "../utils";
+import {
+ constants,
+ AcdpDeploymentTarget,
+ AcdpBuildAction,
+ BuildSourceConfig,
+} from "backstage-plugin-acdp-common";
+import { Location } from "@backstage/catalog-client";
+import {
+ Entity,
+ parseLocationRef,
+ getEntitySourceLocation,
+ getCompoundEntityRef,
+ stringifyEntityRef,
+ ANNOTATION_SOURCE_LOCATION,
+} from "@backstage/catalog-model";
+import { InputError } from "@backstage/errors";
+import { UrlReader } from "@backstage/backend-common";
+import { ScmIntegrations } from "@backstage/integration";
+import { Logger } from "winston";
+import {
+ SSMClient,
+ PutParameterCommand,
+ GetParameterCommand,
+} from "@aws-sdk/client-ssm";
+import * as path from "path";
+
+export class AcdpBuildService {
+ private reader: UrlReader;
+ private integrations: ScmIntegrations;
+ private userAgentString: string;
+ private deploymentTargets: AcdpDeploymentTarget[];
+ private buildParameterSsmPrefix: string;
+ private awsCredentialsProvider: AwsCredentialProvider;
+ private ssmClient: SSMClient;
+ private logger: Logger;
+
+ constructor(options: {
+ config: Config;
+ reader: UrlReader;
+ integrations: ScmIntegrations;
+ awsCredentialsProvider: AwsCredentialProvider;
+ logger: Logger;
+ }) {
+ const { config, reader, integrations, awsCredentialsProvider, logger } =
+ options;
+
+ const defaultDeploymentTarget = {
+ name: constants.ACDP_DEFAULT_DEPLOYMENT_TARGET,
+ awsAccount: config.getString("acdp.deploymentDefaults.accountId"),
+ awsRegion: config.getString("acdp.deploymentDefaults.region"),
+ codeBuildArn: config.getString(
+ "acdp.deploymentDefaults.codeBuildProjectArn",
+ ),
+ };
+ this.deploymentTargets = [defaultDeploymentTarget];
+ this.buildParameterSsmPrefix = config.getString(
+ "acdp.buildConfig.buildConfigStoreSsmPrefix",
+ );
+ this.userAgentString = config.getString("acdp.metrics.userAgentString");
+ this.reader = reader;
+ this.integrations = integrations;
+ this.awsCredentialsProvider = awsCredentialsProvider;
+
+ this.ssmClient = new SSMClient({
+ customUserAgent: this.userAgentString,
+ credentialDefaultProvider: () =>
+ this.awsCredentialsProvider.sdkCredentialProvider,
+ });
+ this.logger = logger;
+ }
+
+ private getDeploymentTargetForEntity(entity: Entity) {
+ const annotations = entity.metadata.annotations!;
+
+ const deploymentTargetName =
+ annotations[constants.ACDP_DEPLOYMENT_TARGET_ANNOTATION];
+
+ if (!deploymentTargetName) {
+ throw new InputError(
+ `No deployment target is set under annotation '${constants.ACDP_DEPLOYMENT_TARGET_ANNOTATION}'`,
+ );
+ }
+
+ const deploymentTarget = this.deploymentTargets.find(
+ (deploymentTarget) => deploymentTarget.name === deploymentTargetName,
+ );
+
+ if (!deploymentTarget) {
+ throw new InputError(
+ `No deployment target found with name '${deploymentTargetName}'`,
+ );
+ }
+
+ return deploymentTarget;
+ }
+
+ private getCodeBuildClient(region: string) {
+ return new CodeBuildClient({
+ region: region,
+ customUserAgent: this.userAgentString,
+ credentialDefaultProvider: () =>
+ this.awsCredentialsProvider.sdkCredentialProvider,
+ });
+ }
+
+ private async getBuildspecForAction(action: AcdpBuildAction, entity: Entity) {
+ const entityAnnotations = entity.metadata.annotations!;
+
+ let actionBuildspecAnnotation: string | undefined = undefined;
+ let actionDefaultBuildspecPath: string | undefined = undefined;
+
+ switch (action) {
+ case AcdpBuildAction.DEPLOY:
+ actionBuildspecAnnotation = constants.ACDP_DEPLOY_BUILDSPEC_ANNOTATION;
+ actionDefaultBuildspecPath =
+ constants.ACDP_DEFAULT_DEPLOY_BUILDSPEC_LOCATION;
+ break;
+ case AcdpBuildAction.UPDATE:
+ actionBuildspecAnnotation = constants.ACDP_UPDATE_BUILDSPEC_ANNOTATION;
+ actionDefaultBuildspecPath =
+ constants.ACDP_DEFAULT_UPDATE_BUILDSPEC_LOCATION;
+ break;
+ case AcdpBuildAction.TEARDOWN:
+ actionBuildspecAnnotation =
+ constants.ACDP_TEARDOWN_BUILDSPEC_ANNOTATION;
+ actionDefaultBuildspecPath =
+ constants.ACDP_DEFAULT_TEARDOWN_BUILDSPEC_LOCATION;
+ break;
+ default:
+ throw new InputError("Invalid ACDP Build Action");
+ }
+
+ let buildspecPath = entityAnnotations[actionBuildspecAnnotation];
+ if (!buildspecPath) {
+ buildspecPath = actionDefaultBuildspecPath;
+ }
+
+ const sourceLocation = getEntitySourceLocation(entity);
+ if (!sourceLocation.target.endsWith("/")) sourceLocation.target += "/";
+ const location = parseLocationRef(buildspecPath) as Location;
+
+ const resolvedLocation = getLocationForEntity(
+ location,
+ sourceLocation.target,
+ this.integrations,
+ false,
+ );
+
+ try {
+ const buildspecBody = await this.reader.readUrl(resolvedLocation.target);
+ return (await buildspecBody.buffer()).toString();
+ } catch (err: any) {
+ if (err.name === "NoSuchKey") {
+ this.logger.error("Buildspec not found");
+ return undefined;
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ public async getProject(entity: Entity): Promise {
+ const deploymentTarget = this.getDeploymentTargetForEntity(entity);
+ const codeBuildClient = this.getCodeBuildClient(deploymentTarget.awsRegion);
+ const { projectName } = this.parseCodeBuildArn(
+ deploymentTarget.codeBuildArn,
+ );
+
+ const projectQueryResult = await codeBuildClient.send(
+ new BatchGetProjectsCommand({
+ names: [projectName],
+ }),
+ );
+ return projectQueryResult.projects?.[0];
+ }
+
+ public async getBuilds(entity: Entity): Promise {
+ const deploymentTarget = this.getDeploymentTargetForEntity(entity);
+ const codeBuildClient = this.getCodeBuildClient(deploymentTarget.awsRegion);
+ const { projectName } = this.parseCodeBuildArn(
+ deploymentTarget.codeBuildArn,
+ );
+
+ const buildIds = await codeBuildClient.send(
+ new ListBuildsForProjectCommand({
+ projectName,
+ }),
+ );
+
+ let builds: Build[] = [];
+
+ if (buildIds.ids) {
+ const output = await codeBuildClient.send(
+ new BatchGetBuildsCommand({
+ ids: buildIds.ids,
+ }),
+ );
+ builds = output.builds ?? [];
+ }
+
+ const filteredBuilds = builds.filter((build: Build) => {
+ const entityUidEnvVar = build.environment?.environmentVariables?.find(
+ (environmentVariable) =>
+ environmentVariable.name ===
+ constants.BACKSTAGE_ENTITY_UID_ENVIRONMENT_VARIABLE,
+ );
+ return entityUidEnvVar?.value === entity.metadata.uid;
+ });
+
+ return filteredBuilds;
+ }
+
+ public async startBuild(options: {
+ entity: Entity;
+ action: AcdpBuildAction;
+ }) {
+ const { entity, action } = options;
+
+ const deploymentTarget = this.getDeploymentTargetForEntity(entity);
+ const codeBuildClient = this.getCodeBuildClient(deploymentTarget.awsRegion);
+ const buildspecBody = await this.getBuildspecForAction(action, entity);
+
+ if (buildspecBody === undefined) {
+ this.logger.error("No buildspec available for action, can't run build");
+ const output: StartBuildCommandOutput = {
+ build: undefined,
+ $metadata: {},
+ };
+ return output;
+ }
+
+ const storedEnvironmentVariables =
+ await this.retrieveBuildEnvironmentVariables(entity);
+
+ const buildSourceConfig = await this.retrieveBuildSourceConfig(entity);
+
+ const environmentVariables =
+ this.updateEnvironmentVariablesForDeploymentTarget(
+ storedEnvironmentVariables,
+ deploymentTarget,
+ entity,
+ );
+
+ return await codeBuildClient.send(
+ new StartBuildCommand({
+ projectName: deploymentTarget.codeBuildArn,
+ buildspecOverride: buildspecBody,
+ environmentVariablesOverride: environmentVariables,
+ sourceTypeOverride: buildSourceConfig.sourceType,
+ sourceLocationOverride: buildSourceConfig.sourceLocation,
+ sourceVersion: buildSourceConfig.sourceVersion,
+ }),
+ );
+ }
+
+ private parseCodeBuildArn(arn: string): {
+ accountId: string;
+ region: string;
+ service: string;
+ resource: string;
+ projectName: string;
+ } {
+ const parsedArn = parse(arn);
+ const resourceParts = parsedArn.resource.split("/");
+ const projectName = resourceParts[1].split(":")[0];
+
+ return { projectName, ...parsedArn };
+ }
+
+ public async storeBuildEnvironmentVariables(
+ entity: Entity,
+ variables: EnvironmentVariable[],
+ ) {
+ const serializedVariables = JSON.stringify(variables);
+ const parameterName =
+ this.getSsmParameterNameForEntityBuildParameters(entity);
+
+ const command = new PutParameterCommand({
+ Name: parameterName,
+ Value: serializedVariables,
+ Type: "SecureString",
+ Overwrite: true,
+ });
+
+ try {
+ await this.ssmClient.send(command);
+ } catch (error) {
+ this.logger.error("Failed to store build parameters in ssm", error);
+ throw new Error("Failed to store build parameters");
+ }
+ }
+
+ private async retrieveBuildEnvironmentVariables(
+ entity: Entity,
+ ): Promise {
+ const parameterName =
+ this.getSsmParameterNameForEntityBuildParameters(entity);
+
+ const command = new GetParameterCommand({
+ Name: parameterName,
+ WithDecryption: true,
+ });
+
+ try {
+ const response = await this.ssmClient.send(command);
+ if (response.Parameter?.Value) {
+ const variables: EnvironmentVariable[] = JSON.parse(
+ response.Parameter.Value,
+ );
+ return variables;
+ }
+ throw new Error(`Parameter '${parameterName}' not found or has no value`);
+ } catch (error) {
+ this.logger.error("Failed to retrieve build parameters from ssm", error);
+ throw new Error("Failed to retrieve build parameters");
+ }
+ }
+
+ protected getSsmParameterNameForEntityBuildParameters(entity: Entity) {
+ const { kind, namespace, name } = getCompoundEntityRef(entity);
+ return path.posix.join(
+ this.buildParameterSsmPrefix,
+ kind.toLowerCase(),
+ namespace.toLowerCase(),
+ name.toLowerCase(),
+ constants.BUILD_PARAMETER_SSM_POSTFIX,
+ );
+ }
+
+ protected getSsmParameterNameForEntitySourceConfig(entity: Entity) {
+ const { kind, namespace, name } = getCompoundEntityRef(entity);
+ return path.posix.join(
+ this.buildParameterSsmPrefix,
+ kind.toLowerCase(),
+ namespace.toLowerCase(),
+ name.toLowerCase(),
+ constants.BUILD_SOURCE_CONFIG_SSM_POSTFIX,
+ );
+ }
+
+ private updateEnvironmentVariablesForDeploymentTarget(
+ environmentVariables: EnvironmentVariable[],
+ deploymentTarget: AcdpDeploymentTarget,
+ entity: Entity,
+ ) {
+ if (!environmentVariables) environmentVariables = [];
+
+ const overrideValues = [
+ {
+ name: "AWS_ACCOUNT_ID",
+ value: deploymentTarget.awsAccount,
+ },
+ {
+ name: "AWS_REGION",
+ value: deploymentTarget.awsRegion,
+ },
+ {
+ name: constants.BACKSTAGE_ENTITY_UID_ENVIRONMENT_VARIABLE,
+ value: entity.metadata.uid,
+ },
+ {
+ name: "BACKSTAGE_ENTITY_REF",
+ value: stringifyEntityRef(entity),
+ },
+ ];
+
+ for (const variableOverride of overrideValues) {
+ const variableIndex = environmentVariables.findIndex(
+ (x) => x.name === variableOverride.name,
+ );
+ if (variableIndex >= 0) {
+ environmentVariables[variableIndex].value = variableOverride.value;
+ } else {
+ environmentVariables.push(variableOverride);
+ }
+ }
+
+ return environmentVariables;
+ }
+
+ public async storeBuildSourceConfig(
+ entity: Entity,
+ config: BuildSourceConfig,
+ ) {
+ const serializedConfig = JSON.stringify(config);
+ const parameterName = this.getSsmParameterNameForEntitySourceConfig(entity);
+
+ const command = new PutParameterCommand({
+ Name: parameterName,
+ Value: serializedConfig,
+ Type: "SecureString",
+ Overwrite: true,
+ });
+
+ try {
+ await this.ssmClient.send(command);
+ } catch (error) {
+ this.logger.error("Failed to store build source config in ssm", error);
+ throw new Error("Failed to store build source config");
+ }
+ }
+
+ private async retrieveBuildSourceConfig(
+ entity: Entity,
+ ): Promise {
+ const parameterName = this.getSsmParameterNameForEntitySourceConfig(entity);
+
+ const command = new GetParameterCommand({
+ Name: parameterName,
+ WithDecryption: true,
+ });
+
+ try {
+ const response = await this.ssmClient.send(command);
+ if (response.Parameter?.Value) {
+ const storedConfig: BuildSourceConfig = JSON.parse(
+ response.Parameter.Value,
+ );
+
+ let config: BuildSourceConfig = storedConfig;
+
+ if (
+ storedConfig.useEntityAssets === true &&
+ entity.metadata.annotations?.[ANNOTATION_SOURCE_LOCATION] !==
+ undefined
+ ) {
+ const catalogItemSourceLocation = removeUrlPrefix(
+ entity.metadata.annotations[ANNOTATION_SOURCE_LOCATION],
+ );
+ const sourceType = getCodeBuildSourceTypeForUrl(
+ catalogItemSourceLocation,
+ );
+ let assetPathCodeBuildLocation: string = catalogItemSourceLocation;
+ if (sourceType == SourceType.S3)
+ assetPathCodeBuildLocation = formatS3UrlToPath(
+ catalogItemSourceLocation,
+ );
+
+ config = {
+ useEntityAssets: true,
+ sourceType: sourceType,
+ sourceLocation: assetPathCodeBuildLocation,
+ };
+ }
+
+ return config;
+ }
+ throw new Error(`Parameter '${parameterName}' not found or has no value`);
+ } catch (error) {
+ this.logger.error(
+ "Failed to retrieve build source config from ssm",
+ error,
+ );
+ throw new Error("Failed to retrieve build source config");
+ }
+ }
+}
+
+function removeUrlPrefix(input: string): string {
+ return input.replace(/^url:/, "");
+}
+
+function getCodeBuildSourceTypeForUrl(url: string): SourceType {
+ const githubPattern = /^https?:\/\/(www\.)?github\.com\/.+\/.+$/;
+ const s3Pattern =
+ /^https?:\/\/s3[\.-](?:[a-z0-9-]+)\.amazonaws\.com\/.+|https?:\/\/[a-z0-9-]+\.s3[\.-](?:[a-z0-9-]+)\.amazonaws\.com\/.+/;
+
+ if (githubPattern.test(url)) {
+ return SourceType.GITHUB;
+ } else if (s3Pattern.test(url)) {
+ return SourceType.S3;
+ } else {
+ return SourceType.NO_SOURCE;
+ }
+}
+
+function formatS3UrlToPath(url: string): string {
+ const urlObject = new URL(url);
+
+ let bucket: string;
+ let path: string = urlObject.pathname.substring(1);
+
+ if (urlObject.hostname.endsWith("s3.amazonaws.com")) {
+ bucket = urlObject.hostname.split(".s3.amazonaws.com")[0];
+ } else {
+ bucket = urlObject.hostname.split(".s3.")[0];
+ }
+
+ return `${bucket}/${path}`;
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/service/router.test.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/router.test.ts
new file mode 100644
index 00000000..b6025f6b
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/router.test.ts
@@ -0,0 +1,199 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { getVoidLogger } from "@backstage/backend-common";
+import express, { NextFunction, Request, Response } from "express";
+import request from "supertest";
+
+import { createRouter } from "./router";
+import { AcdpBuildApi } from "../api/acdp-build-api";
+import { StartBuildInput } from "../utils";
+import { Entity, stringifyEntityRef } from "@backstage/catalog-model";
+import {
+ mockCatalogClient,
+ mockConfig,
+ mockedCatalogEntity,
+ resetMocks,
+} from "../__mocks__/common-mocks";
+import { MockedAcdpBuildService } from "./__mocks__/acdp-build-service.mock";
+import { CatalogClient } from "@backstage/catalog-client";
+import {
+ AcdpBuildAction,
+ AcdpBuildProject,
+ AcdpBuildProjectBuild,
+} from "backstage-plugin-acdp-common";
+
+let app: express.Express;
+const mockIsAuthenticated = (req: Request, _: Response, next: NextFunction) => {
+ req.user = { token: "test-token" };
+ return next();
+};
+
+class MockedAcdpBuildApi extends AcdpBuildApi {
+ public constructor(
+ catalogClient: CatalogClient,
+ acdpBuildService: MockedAcdpBuildService,
+ ) {
+ super(catalogClient, acdpBuildService);
+ }
+
+ public getProject(): Promise {
+ return Promise.resolve({
+ name: "test-project",
+ arn: "arn:aws:codebuild:us-west2:111111111111:project/test",
+ environment: {
+ type: "LINUX_CONTAINER",
+ image: "aws/codebuild/amazonlinux2-x86_64-standard:3.0",
+ computeType: "BUILD_GENERAL1_SMALL",
+ privilegedMode: false,
+ imagePullCredentialsType: "CODEBUILD",
+ },
+ created: new Date("2022-05-20T13:58:29.342000-06:00"),
+ lastModified: new Date("2022-05-20T13:58:29.342000-06:00"),
+ });
+ }
+
+ public getBuilds(entity: Entity): Promise {
+ if (entity === undefined) return Promise.reject();
+
+ return Promise.resolve([
+ {
+ arn: "arn:aws:codebuild:us-west2:111111111111:build/test:test",
+ buildNumber: 1,
+ buildStatus: "SUCCEEDED",
+ currentPhase: "COMPLETED",
+ endTime: new Date("2022-04-14T23:34:38.397Z"),
+ startTime: new Date("2022-04-14T23:31:26.086Z"),
+ },
+ {
+ arn: "arn:aws:codebuild:us-west2:111111111111:build/test:test",
+ buildComplete: true,
+ buildNumber: 2,
+ buildStatus: "SUCCEEDED",
+ currentPhase: "COMPLETED",
+ endTime: new Date("2022-04-14T23:34:38.397Z"),
+ startTime: new Date("2022-04-14T23:31:26.086Z"),
+ },
+ ]);
+ }
+
+ public startBuild(entity: Entity): Promise {
+ return Promise.resolve(entity);
+ }
+
+ public getEntity(
+ entityRef: string,
+ backstageApiToken: string | undefined,
+ ): Promise {
+ return super.getEntity(entityRef, backstageApiToken);
+ }
+}
+
+beforeAll(async () => {
+ const logger = getVoidLogger();
+
+ const acdpBuildService = new MockedAcdpBuildService();
+ const router = await createRouter({
+ logger: logger,
+ config: mockConfig,
+ acdpBuildApi: new MockedAcdpBuildApi(
+ mockCatalogClient(mockedCatalogEntity),
+ acdpBuildService,
+ ),
+ });
+
+ app = express().use(mockIsAuthenticated, router);
+});
+
+beforeEach(() => {
+ resetMocks();
+});
+
+describe("GET /health", () => {
+ it("returns ok", async () => {
+ const response = await request(app).get("/health");
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({ status: "ok" });
+ });
+});
+
+describe("GET /project", () => {
+ it("should return 200 status for valid request", async () => {
+ const response = await request(app).get(
+ `/project?entityRef=${stringifyEntityRef(mockedCatalogEntity)}`,
+ );
+
+ expect(response.status).toEqual(200);
+ });
+});
+
+describe("GET /builds", () => {
+ it("should return 200 status for valid request", async () => {
+ const response = await request(app).get(
+ `/builds?entityRef=${stringifyEntityRef(mockedCatalogEntity)}`,
+ );
+
+ expect(response.status).toEqual(200);
+ });
+
+ it("should return 400 status for missing entityRef", async () => {
+ const response = await request(app).get("/builds");
+
+ expect(response.status).toEqual(400);
+ });
+});
+
+describe("POST /startBuild for deploy", () => {
+ it("should return 200 for request with valid json body", async () => {
+ const requestBody: StartBuildInput = {
+ entityRef: stringifyEntityRef(mockedCatalogEntity),
+ action: AcdpBuildAction.DEPLOY,
+ };
+ await request(app).post("/startBuild").send(requestBody).expect(200);
+ });
+
+ it("should return 400 for request with invalid json body", async () => {
+ const requestBody: StartBuildInput = {
+ entityRef: "bad-value",
+ action: AcdpBuildAction.DEPLOY,
+ };
+ await request(app).post("/startBuild").send(requestBody).expect(400);
+ });
+});
+
+describe("POST /startBuild for Updates", () => {
+ it("should return 200 for request with valid json body", async () => {
+ const requestBody: StartBuildInput = {
+ entityRef: stringifyEntityRef(mockedCatalogEntity),
+ action: AcdpBuildAction.UPDATE,
+ };
+ await request(app).post("/startBuild").send(requestBody).expect(200);
+ });
+
+ it("should return 400 for request with invalid json body", async () => {
+ const requestBody: StartBuildInput = {
+ entityRef: "bad-value",
+ action: AcdpBuildAction.UPDATE,
+ };
+ await request(app).post("/startBuild").send(requestBody).expect(400);
+ });
+});
+
+describe("POST /startBuild for Teardown", () => {
+ it("should return 200 for request with valid json body", async () => {
+ const requestBody: StartBuildInput = {
+ entityRef: stringifyEntityRef(mockedCatalogEntity),
+ action: AcdpBuildAction.TEARDOWN,
+ };
+ await request(app).post("/startBuild").send(requestBody).expect(200);
+ });
+
+ it("should return 400 for request with invalid json body", async () => {
+ const requestBody: StartBuildInput = {
+ entityRef: "bad-value",
+ action: AcdpBuildAction.TEARDOWN,
+ };
+ await request(app).post("/startBuild").send(requestBody).expect(400);
+ });
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/service/router.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/router.ts
new file mode 100644
index 00000000..b6cfe62f
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/router.ts
@@ -0,0 +1,90 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Config } from "@backstage/config";
+import express from "express";
+import Router from "express-promise-router";
+import { Logger } from "winston";
+import { AcdpBuildApi } from "../api";
+import { startBuildInputSchema } from "../utils";
+
+export interface AcdpRouterOptions {
+ logger: Logger;
+ config: Config;
+ acdpBuildApi: AcdpBuildApi;
+}
+
+export async function createRouter(
+ options: AcdpRouterOptions,
+): Promise {
+ const { logger, acdpBuildApi } = options;
+
+ const router = Router();
+ router.use(express.json());
+
+ router.get("/health", (_, response) => {
+ logger.info("PONG!");
+ response.json({ status: "ok" });
+ });
+
+ router.get("/project", async (req, res) => {
+ const entityRef = req.query.entityRef?.toString();
+ const backstageApiToken = req.user?.token;
+
+ if (!entityRef || !isValidEntityRef(entityRef)) {
+ res.status(400).json({ error: "Missing entityRef" });
+ return;
+ }
+
+ const entity = await acdpBuildApi.getEntity(entityRef, backstageApiToken);
+ const response = await acdpBuildApi.getProject(entity);
+
+ res.status(200).json(response);
+ });
+
+ router.get("/builds", async (req, res) => {
+ const entityRef = req.query.entityRef?.toString();
+ const backstageApiToken = req.user?.token;
+
+ if (!entityRef || !isValidEntityRef(entityRef)) {
+ res.status(400).json({ error: "Missing entityRef" });
+ return;
+ }
+
+ const entity = await acdpBuildApi.getEntity(entityRef, backstageApiToken);
+ const response = await acdpBuildApi.getBuilds(entity);
+
+ res.status(200).json(response);
+ });
+
+ router.post("/startBuild", async (req, res) => {
+ const backstageApiToken = req.user?.token;
+ const parsedBody = startBuildInputSchema.safeParse(req.body);
+
+ if (!parsedBody.success) {
+ logger.error(parsedBody.error.errors);
+ return res.status(400).json({ error: parsedBody.error.errors });
+ }
+
+ const entity = await acdpBuildApi.getEntity(
+ parsedBody.data.entityRef,
+ backstageApiToken,
+ );
+ const response = await acdpBuildApi.startBuild(
+ entity,
+ parsedBody.data.action,
+ );
+
+ return res.status(200).json(response);
+ });
+
+ return router;
+}
+
+function isValidEntityRef(input: string): boolean {
+ // Define the regex pattern for validating an entityRef
+ const entityRefPattern = /^[a-zA-Z0-9-_]+:[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+$/;
+
+ // Test the input string against the pattern
+ return entityRefPattern.test(input);
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/service/standaloneServer.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/standaloneServer.ts
new file mode 100644
index 00000000..b08d19ca
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/service/standaloneServer.ts
@@ -0,0 +1,70 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ HostDiscovery,
+ UrlReaders,
+ createServiceBuilder,
+ loadBackendConfig,
+} from "@backstage/backend-common";
+import { Server } from "http";
+import { Logger } from "winston";
+import { createRouter } from "./router";
+import { DefaultAwsCredentialsManager } from "@backstage/integration-aws-node";
+import { AcdpBuildApi } from "backstage-plugin-acdp-backend";
+import { AcdpBuildService } from "./acdp-build-service";
+import { ScmIntegrations } from "@backstage/integration";
+import { CatalogClient } from "@backstage/catalog-client";
+
+export interface ServerOptions {
+ port: number;
+ enableCors: boolean;
+ logger: Logger;
+}
+
+export async function startStandaloneServer(
+ options: ServerOptions,
+): Promise {
+ const logger = options.logger.child({ service: "acdp-backend" });
+ const config = await loadBackendConfig({
+ argv: process.argv,
+ logger: logger,
+ });
+ const integrations = ScmIntegrations.fromConfig(config);
+ const credsManager = DefaultAwsCredentialsManager.fromConfig(config);
+ const discovery = HostDiscovery.fromConfig(config);
+ const urlReader = UrlReaders.default({ logger: logger, config: config });
+ const catalogClient = new CatalogClient({
+ discoveryApi: discovery,
+ });
+
+ const acdpBuildService = new AcdpBuildService({
+ config: config,
+ reader: urlReader,
+ integrations: integrations,
+ awsCredentialsProvider: await credsManager.getCredentialProvider(),
+ logger: logger,
+ });
+ const acdpBuildApi = new AcdpBuildApi(catalogClient, acdpBuildService);
+
+ logger.debug("Starting application server...");
+ const router = await createRouter({
+ logger,
+ config: config,
+ acdpBuildApi: acdpBuildApi,
+ });
+
+ let service = createServiceBuilder(module)
+ .setPort(options.port)
+ .addRouter("/acdp", router);
+ if (options.enableCors) {
+ service = service.enableCors({ origin: "http://localhost:3000" });
+ }
+
+ return await service.start().catch((err) => {
+ logger.error(err);
+ process.exit(1);
+ });
+}
+
+module.hot?.accept();
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/aws-s3-helper.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/aws-s3-helper.ts
new file mode 100644
index 00000000..60eb438f
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/aws-s3-helper.ts
@@ -0,0 +1,182 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ PutObjectCommandInput,
+ DeleteObjectCommand,
+ ListObjectsV2Command,
+ ListObjectsV2CommandOutput,
+ S3Client,
+} from "@aws-sdk/client-s3";
+
+import { Upload } from "@aws-sdk/lib-storage";
+
+import { Logger } from "winston";
+import createLimiter from "p-limit";
+import recursiveReadDir from "recursive-readdir";
+import platformPath from "path";
+import path from "path";
+import fs from "fs";
+import { Entity, DEFAULT_NAMESPACE } from "@backstage/catalog-model";
+
+export class AwsS3Helper {
+ private readonly s3Client: S3Client;
+ private readonly bucketName: string;
+ private readonly logger: Logger;
+ private readonly sse?: "aws:kms" | "AES256";
+
+ constructor(options: {
+ s3Client: S3Client;
+ bucketName: string;
+ logger: Logger;
+ sse?: "aws:kms" | "AES256";
+ }) {
+ this.s3Client = options.s3Client;
+ this.bucketName = options.bucketName;
+ this.logger = options.logger;
+ this.sse = options.sse;
+ }
+
+ async getAllObjectsFromBucket(keyPrefix: string = ""): Promise {
+ const objects: string[] = [];
+ let nextContinuation: string | undefined;
+ let allObjects: ListObjectsV2CommandOutput;
+ // Iterate through every file in the root of the publisher.
+ do {
+ allObjects = await this.s3Client.send(
+ new ListObjectsV2Command({
+ Bucket: this.bucketName,
+ ContinuationToken: nextContinuation,
+ ...(keyPrefix ? { Prefix: keyPrefix } : {}),
+ }),
+ );
+ objects.push(
+ ...(allObjects.Contents || [])
+ .map((f) => f.Key || "")
+ .filter((f) => !!f),
+ );
+ nextContinuation = allObjects.NextContinuationToken;
+ } while (nextContinuation);
+
+ return objects;
+ }
+
+ async deleteObjectsFromBucket(objectsToDelete: string[]) {
+ await bulkStorageOperation(
+ async (relativeFilePath) => {
+ return await this.s3Client.send(
+ new DeleteObjectCommand({
+ Bucket: this.bucketName,
+ Key: relativeFilePath,
+ }),
+ );
+ },
+ objectsToDelete,
+ { concurrencyLimit: 10 },
+ );
+ }
+
+ async uploadFilesToBucket(
+ entity: Entity,
+ localDirectoryPath: string,
+ s3Prefix: string,
+ ) {
+ const objects: string[] = [];
+
+ try {
+ const fileList = await recursiveReadDir(localDirectoryPath).catch(
+ (error: Error) => {
+ throw new Error(
+ `Failed to read fetched content directory: ${error.message}`,
+ );
+ },
+ );
+
+ await bulkStorageOperation(
+ async (absoluteFilePath: string) => {
+ const relativeFilePath = platformPath.relative(
+ localDirectoryPath,
+ absoluteFilePath,
+ );
+ const fileStream = fs.createReadStream(absoluteFilePath);
+
+ const params: PutObjectCommandInput = {
+ Bucket: this.bucketName,
+ Key: path.posix.join(s3Prefix, relativeFilePath),
+ Body: fileStream,
+ ...(this.sse && { ServerSideEncryption: this.sse }),
+ };
+
+ objects.push(params.Key!);
+
+ const upload = new Upload({
+ client: this.s3Client,
+ params,
+ });
+ return upload.done();
+ },
+ fileList,
+ { concurrencyLimit: 10 },
+ );
+
+ this.logger.info(
+ `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${fileList.length}`,
+ );
+ } catch (e) {
+ const errorMessage = `Unable to upload file(s) to AWS S3. ${e}`;
+ this.logger.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+ }
+}
+
+// Perform rate limited generic operations by passing a function and a list of arguments
+const bulkStorageOperation = async (
+ operation: (arg: T) => Promise,
+ args: T[],
+ { concurrencyLimit } = { concurrencyLimit: 25 },
+) => {
+ const limiter = createLimiter(concurrencyLimit);
+ await Promise.all(args.map((arg) => limiter(operation, arg)));
+};
+
+export const getCloudPathForLocalPath = (
+ entity: Entity,
+ localPath = "",
+ externalStorageRootPath = "",
+): string => {
+ const relativeFilePathPosix = localPath.split(path.sep).join(path.posix.sep);
+
+ const entityRootDir = `${entity.metadata?.namespace ?? DEFAULT_NAMESPACE}/${
+ entity.kind
+ }/${entity.metadata.name}`;
+
+ const relativeFilePathTriplet = `${entityRootDir}/${relativeFilePathPosix}`;
+
+ const destination = lowerCaseEntityTriplet(relativeFilePathTriplet);
+
+ const destinationWithRoot = [
+ ...externalStorageRootPath.split(path.posix.sep).filter((s) => s !== ""),
+ destination,
+ ].join("/");
+
+ return destinationWithRoot;
+};
+
+/**
+ * Takes a posix path and returns a lower-cased version of entity's triplet
+ * with the remaining path in posix.
+ *
+ * Path must not include a starting slash.
+ *
+ * @example
+ * lowerCaseEntityTriplet('default/Component/backstage')
+ * // return default/component/backstage
+ */
+const lowerCaseEntityTriplet = (posixPath: string): string => {
+ const [namespace, kind, name, ...rest] = posixPath.split(path.posix.sep);
+ const lowerNamespace = namespace.toLowerCase();
+ const lowerKind = kind.toLowerCase();
+ const lowerName = name.toLowerCase();
+ return [lowerNamespace, lowerKind, lowerName, ...rest].join(path.posix.sep);
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/index.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/index.ts
new file mode 100644
index 00000000..a297cbf9
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/index.ts
@@ -0,0 +1,6 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./validators";
+export * from "./aws-s3-helper";
+export * from "./location-helper";
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/location-helper.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/location-helper.ts
new file mode 100644
index 00000000..a8950390
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/location-helper.ts
@@ -0,0 +1,88 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { ScmIntegrationRegistry } from "@backstage/integration";
+import { Location } from "@backstage/catalog-client";
+
+import { resolveSafeChildPath } from "@backstage/backend-common";
+
+import { InputError } from "@backstage/errors";
+
+import * as path from "path";
+
+export const getLocationForEntity = (
+ location: Location,
+ baseUrl: string,
+ scmIntegration: ScmIntegrationRegistry,
+ allowUnsafeAccess: boolean,
+): Location => {
+ switch (location.type) {
+ case "url":
+ return location;
+ case "dir":
+ return transformDirLocation(
+ baseUrl,
+ location,
+ scmIntegration,
+ allowUnsafeAccess,
+ );
+ default:
+ throw new Error(`Invalid reference location ${location.type}`);
+ }
+};
+
+const transformDirLocation = (
+ baseUrl: string,
+ dirAnnotation: Location,
+ scmIntegrations: ScmIntegrationRegistry,
+ allowUnsafeAccess: boolean,
+): Location => {
+ let locationType = "url";
+ if (baseUrl.startsWith("file://")) locationType = "file";
+
+ switch (locationType) {
+ case "url": {
+ const target = scmIntegrations.resolveUrl({
+ url: dirAnnotation.target,
+ base: baseUrl,
+ });
+
+ return {
+ id: "",
+ type: "url",
+ target,
+ };
+ }
+
+ case "file": {
+ // only permit targets in the same folder as the target of the `file` location
+ const target = resolvePath(
+ path.dirname(baseUrl.slice("file://".length)),
+ dirAnnotation.target,
+ allowUnsafeAccess,
+ );
+
+ return {
+ id: "",
+ type: "dir",
+ target,
+ };
+ }
+
+ default:
+ throw new InputError(`Unable to resolve location type ${locationType}`);
+ }
+};
+
+const resolvePath = (
+ baseUrl: string,
+ assetPath: string,
+ allowUnsafeAccess: boolean,
+) => {
+ if (allowUnsafeAccess) {
+ //skips relative path check for local filesystem access
+ return path.resolve(baseUrl, assetPath);
+ } else {
+ return resolveSafeChildPath(baseUrl, assetPath);
+ }
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/validators.ts b/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/validators.ts
new file mode 100644
index 00000000..34ce0362
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-backend/src/utils/validators.ts
@@ -0,0 +1,17 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { AcdpBuildAction } from "backstage-plugin-acdp-common";
+import { z } from "zod";
+
+export const startBuildInputSchema = z.object({
+ entityRef: z
+ .string()
+ .regex(
+ /^([A-Za-z0-9][-A-Za-z0-9]*):([A-Za-z0-9][-A-Za-z0-9]*|default)\/([A-Za-z0-9_][-A-Za-z0-9_]*)$/,
+ "Invalid EntityRef",
+ ),
+ action: z.nativeEnum(AcdpBuildAction),
+});
+
+export type StartBuildInput = z.infer;
diff --git a/source/modules/acdp/backstage/plugins/acdp-common/.eslintrc.js b/source/modules/acdp/backstage/plugins/acdp-common/.eslintrc.js
new file mode 100644
index 00000000..709e25dd
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-common/.eslintrc.js
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+module.exports = require("@backstage/cli/config/eslint-factory")(__dirname);
diff --git a/source/modules/acdp/backstage/plugins/acdp-common/package.json b/source/modules/acdp/backstage/plugins/acdp-common/package.json
new file mode 100644
index 00000000..64b70ff0
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-common/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "backstage-plugin-acdp-common",
+ "description": "Common interfaces for ACDP plugins",
+ "version": "1.1.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "publishConfig": {
+ "access": "public"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "package.json": [
+ "package.json"
+ ]
+ }
+ },
+ "backstage": {
+ "role": "common-library"
+ },
+ "sideEffects": false,
+ "scripts": {
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/catalog-model": "^1.4.4",
+ "@aws-sdk/client-codebuild": "^3.515.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.25.2"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp-common/src/constants.ts b/source/modules/acdp/backstage/plugins/acdp-common/src/constants.ts
new file mode 100644
index 00000000..5784c3f1
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-common/src/constants.ts
@@ -0,0 +1,30 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export const ACDP_DEPLOY_ON_CREATE_ANNOTATION =
+ "aws.amazon.com/acdp-deploy-on-create";
+export const ACDP_DEPLOYMENT_TARGET_ANNOTATION =
+ "aws.amazon.com/acdp-deployment-target";
+export const ACDP_DEPLOY_BUILDSPEC_ANNOTATION =
+ "aws.amazon.com/acdp-deploy-buildspec";
+export const ACDP_UPDATE_BUILDSPEC_ANNOTATION =
+ "aws.amazon.com/acdp-update-buildspec";
+export const ACDP_TEARDOWN_BUILDSPEC_ANNOTATION =
+ "aws.amazon.com/acdp-teardown-buildspec";
+
+export const BACKSTAGE_TECHDOCS_ANNOTATION = "backstage.io/techdocs-ref";
+
+export const ACDP_DEFAULT_DEPLOYMENT_TARGET = "default";
+export const ACDP_ASSETS_REF = "aws.amazon.com/acdp-assets-ref";
+export const ACDP_ASSETS_STORED = "aws.amazon.com/acdp-assets-stored";
+export const BACKSTAGE_ENTITY_UID_ENVIRONMENT_VARIABLE = "BACKSTAGE_ENTITY_UID";
+
+export const ACDP_DEFAULT_DEPLOY_BUILDSPEC_LOCATION =
+ "dir:./.acdp/deploy.buildspec.yaml";
+export const ACDP_DEFAULT_UPDATE_BUILDSPEC_LOCATION =
+ "dir:./.acdp/update.buildspec.yaml";
+export const ACDP_DEFAULT_TEARDOWN_BUILDSPEC_LOCATION =
+ "dir:./.acdp/teardown.buildspec.yaml";
+
+export const BUILD_PARAMETER_SSM_POSTFIX = "build-parameters";
+export const BUILD_SOURCE_CONFIG_SSM_POSTFIX = "source-config";
diff --git a/source/modules/acdp/backstage/plugins/acdp-common/src/index.ts b/source/modules/acdp/backstage/plugins/acdp-common/src/index.ts
new file mode 100644
index 00000000..8af8f327
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-common/src/index.ts
@@ -0,0 +1,5 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./interfaces/acdp-build";
+export * as constants from "./constants";
diff --git a/source/modules/acdp/backstage/plugins/acdp-common/src/interfaces/acdp-build.ts b/source/modules/acdp/backstage/plugins/acdp-common/src/interfaces/acdp-build.ts
new file mode 100644
index 00000000..e549ff15
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp-common/src/interfaces/acdp-build.ts
@@ -0,0 +1,46 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { SourceType } from "@aws-sdk/client-codebuild";
+import { Entity } from "@backstage/catalog-model";
+
+export interface AcdpDeploymentTarget {
+ name: string;
+ codeBuildArn: string;
+ awsAccount: string;
+ awsRegion: string;
+ codeBuildIamRoleOverrideArn?: string;
+}
+
+export interface AcdpBuildProject {
+ name?: string;
+ arn?: string;
+}
+
+export interface AcdpBuildProjectBuild {
+ id?: string;
+ arn?: string;
+ buildNumber?: number;
+ startTime?: Date;
+ endTime?: Date;
+ currentPhase?: string;
+ buildStatus?: string;
+ projectName?: string;
+}
+
+export enum AcdpBuildAction {
+ DEPLOY = "deploy",
+ UPDATE = "update",
+ TEARDOWN = "teardown",
+}
+
+export interface AcdpBuildInput {
+ entity: Entity;
+}
+
+export interface BuildSourceConfig {
+ useEntityAssets: boolean;
+ sourceType?: SourceType;
+ sourceLocation?: string;
+ sourceVersion?: string;
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp/.eslintrc.js b/source/modules/acdp/backstage/plugins/acdp/.eslintrc.js
new file mode 100644
index 00000000..709e25dd
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/.eslintrc.js
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+module.exports = require("@backstage/cli/config/eslint-factory")(__dirname);
diff --git a/source/modules/acdp/backstage/plugins/acdp/README.md b/source/modules/acdp/backstage/plugins/acdp/README.md
new file mode 100644
index 00000000..a1857c24
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/README.md
@@ -0,0 +1,13 @@
+# acdp
+
+Welcome to the acdp plugin!
+
+_This plugin was created through the Backstage CLI_
+
+## Getting started
+
+Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/acdp](http://localhost:3000/acdp).
+
+You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
+This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
+It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
diff --git a/source/modules/acdp/backstage/plugins/acdp/dev/index.tsx b/source/modules/acdp/backstage/plugins/acdp/dev/index.tsx
new file mode 100644
index 00000000..b443634b
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/dev/index.tsx
@@ -0,0 +1,15 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+import { createDevApp } from "@backstage/dev-utils";
+import { acdpPlugin, EntityAcdpBuildProjectOverviewCard } from "../src/plugin";
+
+createDevApp()
+ .registerPlugin(acdpPlugin)
+ .addPage({
+ element: ,
+ title: "Root Page",
+ path: "/acdp",
+ })
+ .render();
diff --git a/source/modules/acdp/backstage/plugins/acdp/package.json b/source/modules/acdp/backstage/plugins/acdp/package.json
new file mode 100644
index 00000000..9a9f6057
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "backstage-plugin-acdp",
+ "description": "ACDP plugin for Backstage",
+ "version": "1.1.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "frontend-plugin"
+ },
+ "sideEffects": false,
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test --coverage",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@aws-sdk/client-codebuild": "^3.515.0",
+ "@aws-sdk/util-arn-parser": "^3.495.0",
+ "@backstage/catalog-model": "^1.4.4",
+ "@backstage/core-components": "^0.14.0",
+ "@backstage/core-plugin-api": "^1.9.0",
+ "@backstage/errors": "^1.2.3",
+ "@backstage/plugin-catalog-react": "^1.10.0",
+ "@backstage/theme": "^0.5.1",
+ "@material-ui/core": "^4.12.2",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "^4.0.0-alpha.60",
+ "@tanstack/react-query": "^4.36.1",
+ "date-fns": "^2.30.0",
+ "backstage-plugin-acdp-common": "*"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.25.2",
+ "@backstage/core-app-api": "^1.12.0",
+ "@backstage/dev-utils": "^1.0.27",
+ "@backstage/test-utils": "^1.5.0",
+ "@testing-library/jest-dom": "^6.0.0",
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "@testing-library/react": "^14.0.0",
+ "@testing-library/user-event": "^14.0.0",
+ "msw": "^1.0.0",
+ "prettier": "^3.1.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/api/AcdpBuildApi.test.ts b/source/modules/acdp/backstage/plugins/acdp/src/api/AcdpBuildApi.test.ts
new file mode 100644
index 00000000..c4cebfdb
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/api/AcdpBuildApi.test.ts
@@ -0,0 +1,109 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { stringifyEntityRef } from "@backstage/catalog-model";
+import { AcdpBuildApi } from ".";
+import { MockConfigApi } from "@backstage/test-utils";
+import { mockCodeBuildEntity } from "../mocks/mocksCodeBuild";
+import { AcdpBuildAction } from "backstage-plugin-acdp-common";
+
+const baseUrl = "https://example.com";
+
+const acdpBuildApiClient = new AcdpBuildApi({
+ configApi: new MockConfigApi({
+ backend: {
+ baseUrl,
+ },
+ }),
+ identityApi: {
+ getBackstageIdentity: jest.fn(),
+ getCredentials: jest.fn().mockReturnValue({ token: "test" }),
+ getProfileInfo: jest.fn(),
+ signOut: jest.fn(),
+ },
+});
+
+let mockedFetch: jest.SpyInstance;
+beforeEach(() => {
+ mockedFetch = jest.spyOn(global, "fetch").mockImplementation((input) => {
+ const { status, ok } = (input.valueOf() as Request).url.includes(
+ "arn=bad-arn",
+ )
+ ? { status: 404, ok: false }
+ : { status: 200, ok: true };
+ return Promise.resolve({
+ text: () => Promise.resolve(""),
+ status,
+ ok,
+ } as Response);
+ });
+});
+
+afterEach(() => {
+ jest.clearAllMocks();
+});
+
+describe("test", () => {
+ it("should getProject", async () => {
+ await acdpBuildApiClient.getProject({
+ entityRef: stringifyEntityRef(mockCodeBuildEntity),
+ });
+
+ const fetchCall = mockedFetch.mock.calls[0][0].valueOf() as Request;
+ expect(fetchCall.method).toEqual("GET");
+ expect(fetchCall.url).toEqual(
+ `${baseUrl}/api/acdp-backend/project?entityRef=component%3Aacdp%2Fcms-sample`,
+ );
+ });
+
+ it("should listBuilds", async () => {
+ await acdpBuildApiClient.listBuilds({
+ entityRef: stringifyEntityRef(mockCodeBuildEntity),
+ });
+
+ const fetchCall = mockedFetch.mock.calls[0][0].valueOf() as Request;
+ expect(fetchCall.method).toEqual("GET");
+ expect(fetchCall.url).toEqual(
+ `${baseUrl}/api/acdp-backend/builds?entityRef=component%3Aacdp%2Fcms-sample`,
+ );
+ });
+
+ it("should startDeployBuild", async () => {
+ const startBuildInput = {
+ entityRef: stringifyEntityRef(mockCodeBuildEntity),
+ action: AcdpBuildAction.DEPLOY,
+ };
+ await acdpBuildApiClient.startBuild(startBuildInput);
+
+ const fetchCall = mockedFetch.mock.calls[0][0].valueOf() as Request;
+ expect(fetchCall.method).toEqual("POST");
+ expect(fetchCall.url).toEqual(`${baseUrl}/api/acdp-backend/startBuild`);
+ expect(await fetchCall.json()).toStrictEqual(startBuildInput);
+ });
+
+ it("should startUpdateBuild", async () => {
+ const startBuildInput = {
+ entityRef: stringifyEntityRef(mockCodeBuildEntity),
+ action: AcdpBuildAction.UPDATE,
+ };
+ await acdpBuildApiClient.startBuild(startBuildInput);
+
+ const fetchCall = mockedFetch.mock.calls[0][0].valueOf() as Request;
+ expect(fetchCall.method).toEqual("POST");
+ expect(fetchCall.url).toEqual(`${baseUrl}/api/acdp-backend/startBuild`);
+ expect(await fetchCall.json()).toStrictEqual(startBuildInput);
+ });
+
+ it("should startTeardownBuild", async () => {
+ const startBuildInput = {
+ entityRef: stringifyEntityRef(mockCodeBuildEntity),
+ action: AcdpBuildAction.TEARDOWN,
+ };
+ await acdpBuildApiClient.startBuild(startBuildInput);
+
+ const fetchCall = mockedFetch.mock.calls[0][0].valueOf() as Request;
+ expect(fetchCall.method).toEqual("POST");
+ expect(fetchCall.url).toEqual(`${baseUrl}/api/acdp-backend/startBuild`);
+ expect(await fetchCall.json()).toStrictEqual(startBuildInput);
+ });
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/api/AcdpBuildApi.ts b/source/modules/acdp/backstage/plugins/acdp/src/api/AcdpBuildApi.ts
new file mode 100644
index 00000000..9ba7a328
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/api/AcdpBuildApi.ts
@@ -0,0 +1,101 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ IdentityApi,
+ ConfigApi,
+ createApiRef,
+} from "@backstage/core-plugin-api";
+import { ResponseError } from "@backstage/errors";
+import {
+ AcdpBuildAction,
+ AcdpBuildProject,
+ AcdpBuildProjectBuild,
+} from "backstage-plugin-acdp-common";
+
+export const acdpBuildApiRef = createApiRef({
+ id: "plugin.acdpbuild.service",
+});
+
+export interface StartBuildInput {
+ entityRef: string;
+ action: AcdpBuildAction;
+}
+
+export class AcdpBuildApi {
+ private readonly configApi: ConfigApi;
+ private readonly identityApi: IdentityApi;
+
+ public constructor(options: {
+ configApi: ConfigApi;
+ identityApi: IdentityApi;
+ }) {
+ this.configApi = options.configApi;
+ this.identityApi = options.identityApi;
+ }
+
+ async getProject({
+ entityRef,
+ }: {
+ entityRef: string;
+ }): Promise {
+ const searchParams = new URLSearchParams({
+ entityRef: entityRef,
+ });
+ const urlSegment = `/project?${searchParams}`;
+
+ return await this.fetch(urlSegment);
+ }
+
+ async listBuilds({
+ entityRef,
+ }: {
+ entityRef: string;
+ }): Promise {
+ const searchParams = new URLSearchParams({
+ entityRef: entityRef,
+ });
+ const urlSegment = `/builds?${searchParams}`;
+
+ return await this.fetch(urlSegment);
+ }
+
+ async startBuild(input: StartBuildInput): Promise {
+ return await this.fetch("/startBuild", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(input),
+ });
+ }
+
+ private async fetch(input: string, init?: RequestInit): Promise {
+ const baseUrl = `${this.configApi.getString(
+ "backend.baseUrl",
+ )}/api/acdp-backend`;
+
+ const { token: idToken } = await this.identityApi.getCredentials();
+
+ const headers: HeadersInit = new Headers(init?.headers);
+ if (idToken && !headers.has("authorization")) {
+ headers.set("authorization", `Bearer ${idToken}`);
+ }
+
+ const request = new Request(`${baseUrl}${input}`, {
+ ...init,
+ headers,
+ });
+
+ return fetch(request).then(async (response) => {
+ if (!response.ok) {
+ throw await ResponseError.fromResponse(response);
+ }
+
+ const text = await response.text();
+ if (text != undefined && text.length > 0) {
+ return JSON.parse(text);
+ } else {
+ return undefined;
+ }
+ });
+ }
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/api/index.ts b/source/modules/acdp/backstage/plugins/acdp/src/api/index.ts
new file mode 100644
index 00000000..da511835
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/api/index.ts
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./AcdpBuildApi";
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/AboutField/AboutField.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/AboutField/AboutField.tsx
new file mode 100644
index 00000000..cf0b2d8f
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/AboutField/AboutField.tsx
@@ -0,0 +1,56 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Grid, makeStyles, Typography } from "@material-ui/core";
+import React from "react";
+
+interface AboutFieldProps {
+ label: string;
+ gridSizes?: Record;
+ children?: React.ReactNode;
+}
+
+const useStyles = makeStyles((theme) => ({
+ links: {
+ margin: theme.spacing(2, 0),
+ display: "grid",
+ gridAutoFlow: "column",
+ gridAutoColumns: "min-content",
+ gridGap: theme.spacing(3),
+ },
+ label: {
+ color: theme.palette.text.secondary,
+ textTransform: "uppercase",
+ fontSize: "10px",
+ fontWeight: "bold",
+ letterSpacing: 0.5,
+ overflow: "hidden",
+ whiteSpace: "nowrap",
+ },
+ value: {
+ fontWeight: "bold",
+ overflow: "hidden",
+ lineHeight: "24px",
+ wordBreak: "break-word",
+ },
+ description: {
+ wordBreak: "break-word",
+ },
+}));
+
+export const AboutField = (props: AboutFieldProps) => {
+ const { label, gridSizes, children } = props;
+
+ const classes = useStyles();
+
+ return (
+
+
+ {children}
+
+ );
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/AboutField/index.ts b/source/modules/acdp/backstage/plugins/acdp/src/components/AboutField/index.ts
new file mode 100644
index 00000000..a05beab1
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/AboutField/index.ts
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./AboutField";
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/BuildStatus/BuildStatus.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/BuildStatus/BuildStatus.tsx
new file mode 100644
index 00000000..44ea088e
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/BuildStatus/BuildStatus.tsx
@@ -0,0 +1,62 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ StatusRunning,
+ StatusOK,
+ StatusAborted,
+ StatusError,
+} from "@backstage/core-components";
+import { StatusType } from "@aws-sdk/client-codebuild";
+import React from "react";
+
+interface BuildStatusProps {
+ status?: string;
+}
+
+export const BuildStatus = (props: BuildStatusProps) => {
+ switch (props.status) {
+ case StatusType.IN_PROGRESS:
+ return (
+ <>
+ In progress
+ >
+ );
+ case StatusType.FAULT:
+ return (
+ <>
+ Fault
+ >
+ );
+ case StatusType.TIMED_OUT:
+ return (
+ <>
+ Timed out
+ >
+ );
+ case StatusType.FAILED:
+ return (
+ <>
+ Failed
+ >
+ );
+ case StatusType.SUCCEEDED:
+ return (
+ <>
+ Succeeded
+ >
+ );
+ case StatusType.STOPPED:
+ return (
+ <>
+ Stopped
+ >
+ );
+ default:
+ return (
+ <>
+ Unknown
+ >
+ );
+ }
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/BuildStatus/index.ts b/source/modules/acdp/backstage/plugins/acdp/src/components/BuildStatus/index.ts
new file mode 100644
index 00000000..64484222
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/BuildStatus/index.ts
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./BuildStatus";
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/BuildHistoryTable.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/BuildHistoryTable.tsx
new file mode 100644
index 00000000..ea31de7d
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/BuildHistoryTable.tsx
@@ -0,0 +1,79 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+import { Link } from "@material-ui/core";
+
+import { Table, TableColumn } from "@backstage/core-components";
+
+import { formatDistanceStrict } from "date-fns";
+
+import { BuildStatus } from "../BuildStatus";
+import { AcdpBuildProjectBuild } from "backstage-plugin-acdp-common";
+
+interface BuildHistoryTableProps {
+ region: string;
+ accountId: string;
+ project: string | undefined;
+ builds: AcdpBuildProjectBuild[];
+ buildHistoryLength: number;
+}
+
+interface IndexedBuild extends AcdpBuildProjectBuild {
+ index?: number;
+}
+
+export const BuildHistoryTable = (props: BuildHistoryTableProps) => {
+ const { region, accountId, project, builds, buildHistoryLength } = props;
+ const indexedBuilds = (builds.slice(0, buildHistoryLength) ?? []).map(
+ (build, index) => ({ ...build, index: builds.length - index }),
+ );
+
+ const columns: TableColumn[] = [
+ {
+ title: "Module Build Number",
+ field: "moduleBuildNumber",
+ render: (row: IndexedBuild) => `#${row.index}`,
+ },
+ {
+ title: "Project Build Number",
+ field: "projectBuildNumber",
+ render: (row: IndexedBuild) => {
+ return (
+
+ #{row.buildNumber}
+
+ );
+ },
+ },
+ {
+ title: "Status",
+ field: "deploymentStatus",
+ render: (row: IndexedBuild) => ,
+ },
+ {
+ title: "Duration",
+ field: "duration",
+ render: (row: IndexedBuild) =>
+ row.startTime && row.endTime
+ ? formatDistanceStrict(new Date(row.endTime), new Date(row.startTime))
+ : "",
+ },
+ ];
+
+ return (
+
+ );
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/CodeBuildWidget.test.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/CodeBuildWidget.test.tsx
new file mode 100644
index 00000000..579b8af8
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/CodeBuildWidget.test.tsx
@@ -0,0 +1,106 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AnyApiRef } from "@backstage/core-plugin-api";
+import { EntityProvider } from "@backstage/plugin-catalog-react";
+import {
+ setupRequestMockHandlers,
+ TestApiProvider,
+ wrapInTestApp,
+} from "@backstage/test-utils";
+import { act, render, waitFor } from "@testing-library/react";
+import { setupServer } from "msw/node";
+import React from "react";
+import { acdpBuildApiRef } from "../../api";
+import {
+ mockCodeBuildEntity,
+ MockCodeBuildService,
+} from "../../mocks/mocksCodeBuild";
+import { AcdpBuildWidget } from "./CodeBuildWidget";
+
+const apis: [AnyApiRef, Partial][] = [
+ [acdpBuildApiRef, new MockCodeBuildService()],
+];
+
+describe("AcdpBuildWidget", () => {
+ const worker = setupServer();
+ setupRequestMockHandlers(worker);
+
+ it("should display widget when ARN is present", async () => {
+ const rendered = render(
+ wrapInTestApp(
+
+
+
+
+ ,
+ ),
+ );
+ expect(await rendered.findByText("test-project")).toBeInTheDocument();
+ expect(await rendered.findAllByText("Succeeded")).toHaveLength(3);
+ });
+
+ it("should load and refresh on update button click", async () => {
+ const rendered = render(
+ wrapInTestApp(
+
+
+
+
+ ,
+ ),
+ );
+ expect(await rendered.findByText("test-project")).toBeInTheDocument();
+ (await rendered.findByText("Update")).click();
+ await waitFor(() => {
+ expect(rendered.getByRole("progressbar")).toBeInTheDocument();
+ });
+ await waitFor(() => {
+ expect(rendered.queryByRole("progressbar")).not.toBeInTheDocument();
+ });
+ expect(await rendered.findAllByText("Succeeded")).toHaveLength(3);
+ });
+
+ it("should load and refresh on teardown button click", async () => {
+ const rendered = render(
+ wrapInTestApp(
+
+
+
+
+ ,
+ ),
+ );
+ expect(await rendered.findByText("test-project")).toBeInTheDocument();
+ await act(async () => {
+ (await rendered.findByText("Teardown")).click();
+ });
+ await waitFor(async () => {
+ expect(await rendered.findByText("Confirm")).toBeInTheDocument();
+ });
+ await act(async () => {
+ (await rendered.findByText("Confirm")).click();
+ });
+
+ await waitFor(async () => {
+ expect(rendered.getByRole("progressbar")).toBeInTheDocument();
+ });
+ await waitFor(() => {
+ expect(rendered.queryByRole("progressbar")).not.toBeInTheDocument();
+ });
+ expect(await rendered.findAllByText("Succeeded")).toHaveLength(3);
+ });
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/CodeBuildWidget.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/CodeBuildWidget.tsx
new file mode 100644
index 00000000..b8f222a3
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/CodeBuildWidget.tsx
@@ -0,0 +1,57 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+import { IconButton } from "@material-ui/core";
+import { Cached } from "@material-ui/icons";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+import { InfoCard } from "@backstage/core-components";
+import {
+ useEntity,
+ MissingAnnotationEmptyState,
+} from "@backstage/plugin-catalog-react";
+
+import { WidgetContent } from "./WidgetContent";
+import { constants } from "backstage-plugin-acdp-common";
+import { isAcdpBuildProjectAvailable } from "../Flags";
+
+export interface AcdpBuildWidgetProps {
+ buildHistoryLength?: number;
+}
+
+const queryClient = new QueryClient();
+
+export const AcdpBuildWidget = (props: AcdpBuildWidgetProps) => {
+ const { buildHistoryLength = 3 } = props;
+ const { entity } = useEntity();
+
+ return (
+
+ {!isAcdpBuildProjectAvailable(entity) ? (
+
+ ) : (
+
+ queryClient.refetchQueries({
+ queryKey: ["getCodeBuildProjectBuilds"],
+ })
+ }
+ >
+
+
+ }
+ >
+
+
+ )}
+
+ );
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/MostRecentBuild.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/MostRecentBuild.tsx
new file mode 100644
index 00000000..3d7a4ffe
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/MostRecentBuild.tsx
@@ -0,0 +1,75 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+import { Box, Grid, Link } from "@material-ui/core";
+
+import { formatDistanceStrict } from "date-fns";
+
+import { AboutField } from "../AboutField";
+import { BuildStatus } from "../BuildStatus";
+
+import { parseCodeBuildArn } from "../../utils";
+import {
+ AcdpBuildProject,
+ AcdpBuildProjectBuild,
+} from "backstage-plugin-acdp-common";
+
+const projectMostRecentBuildStatus = (builds: AcdpBuildProjectBuild[]) => {
+ return builds.length > 0 ? (
+
+ ) : (
+ <>>
+ );
+};
+
+const projectMostRecentBuildExecuted = (builds: AcdpBuildProjectBuild[]) => {
+ const build = builds.find((b) => b.startTime);
+ return build
+ ? `${formatDistanceStrict(new Date(build.startTime!), new Date())} ago`
+ : "";
+};
+
+const projectMostRecentBuildDuration = (builds: AcdpBuildProjectBuild[]) => {
+ const build = builds.find((b) => b.startTime && b.endTime);
+ return build
+ ? formatDistanceStrict(new Date(build.startTime!), new Date(build.endTime!))
+ : "";
+};
+
+interface MostRecentBuildProps {
+ project: AcdpBuildProject;
+ builds: AcdpBuildProjectBuild[];
+}
+
+export const MostRecentBuild = (props: MostRecentBuildProps) => {
+ const { project, builds } = props;
+ const { accountId, region } = parseCodeBuildArn(project.arn!);
+
+ return (
+
+
+
+
+ {project.name}
+
+
+
+ {projectMostRecentBuildStatus(builds)}
+
+
+ {projectMostRecentBuildExecuted(builds)}
+
+
+ {projectMostRecentBuildDuration(builds)}
+
+
+
+ );
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/TeardownConfirmDialog.test.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/TeardownConfirmDialog.test.tsx
new file mode 100644
index 00000000..8585cb9d
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/TeardownConfirmDialog.test.tsx
@@ -0,0 +1,194 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+jest.mock("./useTeardownConfirmDialogState");
+
+import userEvent from "@testing-library/user-event";
+import React from "react";
+import { TeardownConfirmDialog } from "./TeardownConfirmDialog";
+import { screen, waitFor } from "@testing-library/react";
+import { renderInTestApp, TestApiProvider } from "@backstage/test-utils";
+import * as state from "./useTeardownConfirmDialogState";
+
+import { AlertApi, alertApiRef } from "@backstage/core-plugin-api";
+import { stringifyEntityRef } from "@backstage/catalog-model";
+import { entityRouteRef } from "@backstage/plugin-catalog-react";
+
+describe("TeardownConfirmDialog", () => {
+ const alertApi: AlertApi = {
+ post() {
+ return undefined;
+ },
+ alert$() {
+ throw new Error("not implemented");
+ },
+ };
+
+ beforeEach(() => {
+ jest.spyOn(alertApi, "post").mockImplementation(() => {});
+ });
+
+ const entity = {
+ apiVersion: "backstage.io/v1alpha1",
+ kind: "Component",
+ metadata: {
+ name: "n",
+ namespace: "ns",
+ annotations: {},
+ },
+ spec: {},
+ };
+
+ const Wrapper = (props: { children?: React.ReactNode }) => (
+
+ {props.children}
+
+ );
+
+ const stateSpy = jest.spyOn(state, "useTeardownConfirmDialogState");
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it("can cancel", async () => {
+ const onClose = jest.fn();
+ stateSpy.mockImplementation(() => ({
+ type: "teardown",
+ entityRef: stringifyEntityRef(entity),
+ teardownEntity: jest.fn(),
+ }));
+
+ await renderInTestApp(
+
+ {}}
+ entity={entity}
+ />
+ ,
+ {
+ mountedRoutes: {
+ "/catalog/:namespace/:kind/:name/*": entityRouteRef,
+ },
+ },
+ );
+
+ await userEvent.click(screen.getByText("Cancel"));
+
+ await waitFor(() => {
+ expect(onClose).toHaveBeenCalled();
+ });
+ });
+
+ it("handles the loading state", async () => {
+ stateSpy.mockImplementation(() => ({ type: "loading" }));
+
+ await renderInTestApp(
+
+ {}}
+ onConfirm={() => {}}
+ entity={entity}
+ />
+ ,
+ {
+ mountedRoutes: {
+ "/catalog/:namespace/:kind/:name/*": entityRouteRef,
+ },
+ },
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId("progress")).toBeInTheDocument();
+ });
+ });
+
+ it("handles the error state", async () => {
+ stateSpy.mockImplementation(() => ({
+ type: "error",
+ error: new TypeError("eek!"),
+ }));
+
+ await renderInTestApp(
+
+ {}}
+ onConfirm={() => {}}
+ entity={entity}
+ />
+ ,
+ {
+ mountedRoutes: {
+ "/catalog/:namespace/:kind/:name/*": entityRouteRef,
+ },
+ },
+ );
+
+ await waitFor(() => {
+ expect(screen.getAllByText("eek!").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("TypeError").length).toBeGreaterThan(0);
+ });
+ });
+
+ it("handles the unregister state, choosing to unregister", async () => {
+ const teardownEntity = jest.fn();
+ const onConfirm = jest.fn();
+
+ stateSpy.mockImplementation(() => ({
+ type: "teardown",
+ entityRef: stringifyEntityRef(entity),
+ teardownEntity,
+ }));
+
+ await renderInTestApp(
+
+ {}}
+ onConfirm={onConfirm}
+ entity={entity}
+ />
+ ,
+ {
+ mountedRoutes: {
+ "/catalog/:namespace/:kind/:name/*": entityRouteRef,
+ },
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ /This action will run the teardown build for the following entity/,
+ ),
+ ).toBeInTheDocument();
+ expect(screen.getByText(stringifyEntityRef(entity))).toBeInTheDocument();
+ });
+
+ await userEvent.click(screen.getByText("Confirm"));
+
+ await waitFor(() => {
+ expect(teardownEntity).toHaveBeenCalled();
+ expect(onConfirm).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/TeardownConfirmDialog.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/TeardownConfirmDialog.tsx
new file mode 100644
index 00000000..b64654a0
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/TeardownConfirmDialog.tsx
@@ -0,0 +1,144 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity } from "@backstage/catalog-model";
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ makeStyles,
+} from "@material-ui/core";
+import Alert from "@material-ui/lab/Alert";
+import React, { useCallback, useState } from "react";
+import { useTeardownConfirmDialogState } from "./useTeardownConfirmDialogState";
+
+import { alertApiRef, useApi } from "@backstage/core-plugin-api";
+import { Progress, ResponseErrorPanel } from "@backstage/core-components";
+import { assertError } from "@backstage/errors";
+
+const useStyles = makeStyles({
+ advancedButton: {
+ fontSize: "0.7em",
+ },
+ dialogActions: {
+ display: "inline-block",
+ },
+});
+
+const Contents = ({
+ entity,
+ onConfirm,
+ onClose,
+}: {
+ entity: Entity;
+ onConfirm: () => any;
+ onClose: () => any;
+}) => {
+ const alertApi = useApi(alertApiRef);
+ const classes = useStyles();
+ const state = useTeardownConfirmDialogState(entity);
+ const [busy, setBusy] = useState(false);
+
+ const onTeardown = useCallback(
+ async function onTeardownFn() {
+ if ("teardownEntity" in state) {
+ setBusy(true);
+ try {
+ state.teardownEntity();
+ onConfirm();
+ } catch (err) {
+ assertError(err);
+ alertApi.post({ message: err.message });
+ } finally {
+ setBusy(false);
+ }
+ }
+ },
+ [alertApi, onConfirm, state],
+ );
+
+ const DialogActionsPanel = () => (
+
+
+
+ );
+
+ if (state.type === "loading") {
+ return ;
+ }
+
+ if (state.type === "error") {
+ return ;
+ }
+
+ if (state.type === "teardown") {
+ return (
+ <>
+
+ This action will run the teardown build for the following entity:
+
+
+ {state.entityRef}
+
+
+ To redeploy, you must unregister and then re-create the entity.
+
+
+
+
+
+ >
+ );
+ }
+
+ return Internal error: Unknown state;
+};
+
+export type TeardownConfirmDialogProps = {
+ open: boolean;
+ onConfirm: () => any;
+ onClose: () => any;
+ entity: Entity;
+};
+
+export const TeardownConfirmDialog = (props: TeardownConfirmDialogProps) => {
+ const { open, onConfirm, onClose, entity } = props;
+ return (
+
+ );
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/index.ts b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/index.ts
new file mode 100644
index 00000000..84c6124d
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/index.ts
@@ -0,0 +1,5 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export { TeardownConfirmDialog } from "./TeardownConfirmDialog";
+export type { TeardownConfirmDialogProps } from "./TeardownConfirmDialog";
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/useTeardownConfirmDialogState.test.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/useTeardownConfirmDialogState.test.tsx
new file mode 100644
index 00000000..17a6971e
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/useTeardownConfirmDialogState.test.tsx
@@ -0,0 +1,57 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity, stringifyEntityRef } from "@backstage/catalog-model";
+import { renderHook, waitFor } from "@testing-library/react";
+import { useTeardownConfirmDialogState } from "./useTeardownConfirmDialogState";
+
+describe("useTeardownConfirmDialogState", () => {
+ let entity: Entity;
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+
+ entity = {
+ apiVersion: "backstage.io/v1alpha1",
+ kind: "Component",
+ metadata: {
+ name: "n",
+ namespace: "ns",
+ annotations: {},
+ },
+ spec: {},
+ };
+ });
+
+ it("goes through the confirm path", async () => {
+ const rendered = renderHook(
+ () => useTeardownConfirmDialogState(entity),
+ {},
+ );
+
+ expect(rendered.result.current).toEqual({ type: "loading" });
+
+ await waitFor(() => {
+ expect(rendered.result.current).toEqual({
+ type: "teardown",
+ entityRef: stringifyEntityRef(entity),
+ teardownEntity: expect.any(Function),
+ });
+ });
+ });
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/useTeardownConfirmDialogState.ts b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/useTeardownConfirmDialogState.ts
new file mode 100644
index 00000000..381942fa
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/TeardownConfirmDialog/useTeardownConfirmDialogState.ts
@@ -0,0 +1,74 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity, stringifyEntityRef } from "@backstage/catalog-model";
+import { useCallback } from "react";
+import useAsync from "react-use/lib/useAsync";
+
+/**
+ * Each distinct state that the dialog can be in at any given time.
+ */
+export type UseTeardownConfirmDialogState =
+ | {
+ type: "loading";
+ }
+ | {
+ type: "error";
+ error: Error;
+ }
+ | {
+ type: "teardown";
+ entityRef: string;
+ teardownEntity: () => boolean;
+ };
+
+/**
+ * Houses the main logic for unregistering entities and their locations.
+ */
+export function useTeardownConfirmDialogState(
+ entity: Entity,
+): UseTeardownConfirmDialogState {
+ const entityRef = stringifyEntityRef(entity);
+
+ // Load the prerequisite data: what entities that are colocated with us, and
+ // what location that spawned us
+ const prerequisites = useAsync(async () => {
+ //todo: fetch CFN template status here.
+ }, [entity]);
+
+ const teardownEntity = useCallback(
+ function teardownEntityConfirm() {
+ return true;
+ },
+ [prerequisites],
+ );
+
+ // Return early if prerequisites still loading or failing
+ const { loading, error } = prerequisites;
+ if (loading) {
+ return { type: "loading" };
+ } else if (error) {
+ return { type: "error", error };
+ }
+
+ return {
+ type: "teardown",
+ entityRef: entityRef,
+ teardownEntity: teardownEntity,
+ };
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/WidgetContent.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/WidgetContent.tsx
new file mode 100644
index 00000000..d55c2fd8
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/WidgetContent.tsx
@@ -0,0 +1,155 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React, { useState } from "react";
+import { Button, LinearProgress } from "@material-ui/core";
+import {
+ useMutation,
+ useIsMutating,
+ useQueryClient,
+ useQuery,
+} from "@tanstack/react-query";
+
+import { useEntity } from "@backstage/plugin-catalog-react";
+import { useApi } from "@backstage/core-plugin-api";
+
+import { MostRecentBuild } from "./MostRecentBuild";
+import { BuildHistoryTable } from "./BuildHistoryTable";
+import { StartBuildInput, acdpBuildApiRef } from "../../api";
+import { ResponseErrorPanel } from "@backstage/core-components";
+import { stringifyEntityRef } from "@backstage/catalog-model";
+import { parseCodeBuildArn } from "../../utils";
+import { TeardownConfirmDialog } from "./TeardownConfirmDialog/TeardownConfirmDialog";
+import { AcdpBuildAction } from "backstage-plugin-acdp-common";
+
+interface WidgetContentProps {
+ buildHistoryLength: number;
+}
+
+export const WidgetContent = (props: WidgetContentProps) => {
+ const { buildHistoryLength } = props;
+ const api = useApi(acdpBuildApiRef);
+ const { entity } = useEntity();
+ const entityRef = stringifyEntityRef(entity);
+ const queryClient = useQueryClient();
+
+ const getCodeBuildProjectBuildsQuery = useQuery({
+ queryKey: ["getCodeBuildProjectBuilds"],
+ queryFn: async () => {
+ const project = await api.getProject({ entityRef: entityRef });
+
+ if (!project || !project.arn) {
+ throw new Error("No CodeBuild Project Found");
+ }
+
+ const builds = await api.listBuilds({ entityRef: entityRef });
+ const { accountId, region } = parseCodeBuildArn(project.arn!);
+
+ return { project, builds, accountId, region };
+ },
+ });
+
+ const startUpdateBuildMutation = useMutation({
+ mutationKey: ["startCodeBuild"],
+ mutationFn: (input: StartBuildInput) => api.startBuild(input),
+ onSuccess: () =>
+ queryClient.invalidateQueries({
+ queryKey: ["getCodeBuildProjectBuilds"],
+ }),
+ });
+
+ const startTeardownBuildMutation = useMutation({
+ mutationKey: ["startCodeBuild"],
+ mutationFn: (input: StartBuildInput) => api.startBuild(input),
+ onSuccess: () =>
+ queryClient.invalidateQueries({
+ queryKey: ["getCodeBuildProjectBuilds"],
+ }),
+ });
+
+ const startCodeBuildMutationCount = useIsMutating({
+ mutationKey: ["startCodeBuild"],
+ });
+
+ const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
+
+ const cleanUpAfterTeardown = async () => {
+ setConfirmationDialogOpen(false);
+ startTeardownBuildMutation.mutate({
+ entityRef: entityRef,
+ action: AcdpBuildAction.TEARDOWN,
+ });
+ //todo: on success, unregister the entity?
+ };
+
+ return (
+ <>
+ {(getCodeBuildProjectBuildsQuery.isFetching ||
+ startCodeBuildMutationCount > 0) && }
+ {getCodeBuildProjectBuildsQuery.isSuccess &&
+ getCodeBuildProjectBuildsQuery.data?.project &&
+ getCodeBuildProjectBuildsQuery.data.builds && (
+ <>
+
+ {buildHistoryLength > 0 && (
+
+ )}
+
+
+
+ >
+ )}
+ {getCodeBuildProjectBuildsQuery.isError && (
+
+ )}
+ setConfirmationDialogOpen(false)}
+ />
+ >
+ );
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/index.ts b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/index.ts
new file mode 100644
index 00000000..045fbc5f
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/CodeBuildWidget/index.ts
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./CodeBuildWidget";
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/components/Flags.tsx b/source/modules/acdp/backstage/plugins/acdp/src/components/Flags.tsx
new file mode 100644
index 00000000..9e4dfab0
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/components/Flags.tsx
@@ -0,0 +1,11 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Entity } from "@backstage/catalog-model";
+import { constants } from "backstage-plugin-acdp-common";
+
+export const isAcdpBuildProjectAvailable = (entity: Entity) => {
+ return Boolean(
+ entity.metadata.annotations?.[constants.ACDP_DEPLOYMENT_TARGET_ANNOTATION],
+ );
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/index.ts b/source/modules/acdp/backstage/plugins/acdp/src/index.ts
new file mode 100644
index 00000000..10939550
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/index.ts
@@ -0,0 +1,6 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./plugin";
+export * from "./components/Flags";
+export * from "./api";
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/mocks/mocksCodeBuild.ts b/source/modules/acdp/backstage/plugins/acdp/src/mocks/mocksCodeBuild.ts
new file mode 100644
index 00000000..06b27a81
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/mocks/mocksCodeBuild.ts
@@ -0,0 +1,153 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+/**
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Build,
+ Project,
+ StartBuildCommandOutput,
+} from "@aws-sdk/client-codebuild";
+import { Entity } from "@backstage/catalog-model";
+import { StartBuildInput } from "../api";
+
+export class MockCodeBuildService {
+ async getProject(): Promise {
+ return {
+ name: "test-project",
+ arn: "arn:aws:codebuild:us-west-2:111111111111:project/test-project",
+ environment: {
+ type: "LINUX_CONTAINER",
+ image: "aws/codebuild/amazonlinux2-x86_64-standard:3.0",
+ computeType: "BUILD_GENERAL1_SMALL",
+ privilegedMode: false,
+ imagePullCredentialsType: "CODEBUILD",
+ },
+ created: new Date("2022-05-20T13:58:29.342000-06:00"),
+ lastModified: new Date("2022-05-20T13:58:29.342000-06:00"),
+ };
+ }
+
+ async listBuilds({ stackName }: { stackName: string }): Promise {
+ return [
+ {
+ arn: "arn:aws:codebuild:us-west-2:111111111111:project/test-project",
+ buildComplete: true,
+ buildNumber: 1,
+ buildStatus: "SUCCEEDED",
+ currentPhase: "COMPLETED",
+ endTime: new Date("2022-04-14T23:34:38.397Z"),
+ startTime: new Date("2022-04-14T23:31:26.086Z"),
+ environment: {
+ computeType: "BUILD_GENERAL1_SMALL",
+ environmentVariables: [],
+ image: "aws/codebuild/standard:5.0",
+ imagePullCredentialsType: "CODEBUILD",
+ privilegedMode: false,
+ type: "LINUX_CONTAINER",
+ },
+ exportedEnvironmentVariables: [
+ {
+ name: "MODULE_STACK_NAME",
+ value: stackName,
+ },
+ ],
+ },
+ {
+ arn: "arn:aws:codebuild:us-west-2:111111111111:project/test-project",
+ buildComplete: true,
+ buildNumber: 2,
+ buildStatus: "SUCCEEDED",
+ currentPhase: "COMPLETED",
+ endTime: new Date("2022-04-14T23:34:38.397Z"),
+ startTime: new Date("2022-04-14T23:31:26.086Z"),
+ environment: {
+ computeType: "BUILD_GENERAL1_SMALL",
+ environmentVariables: [],
+ image: "aws/codebuild/standard:5.0",
+ imagePullCredentialsType: "CODEBUILD",
+ privilegedMode: false,
+ type: "LINUX_CONTAINER",
+ },
+ exportedEnvironmentVariables: [
+ {
+ name: "MODULE_STACK_NAME",
+ value: stackName,
+ },
+ ],
+ },
+ ];
+ }
+
+ async startBuild(_: StartBuildInput): Promise {
+ // Wait for 1 second so that progress bar element can be properly tested
+ await new Promise((r) => setTimeout(r, 1001));
+ return {
+ $metadata: {},
+ };
+ }
+}
+
+export const mockCodeBuildEntity: Entity = {
+ apiVersion: "backstage.io/v1alpha1",
+ kind: "Component",
+ metadata: {
+ uid: "uniqueId",
+ annotations: {
+ "aws.amazon.com/acdp-deploy-on-create": "true",
+ "aws.amazon.com/acdp-deployment-target": "default",
+ "aws.amazon.com/techdocs-builder": "external",
+ "backstage.io/techdocs-ref": "dir:.",
+ "aws.amazon.com/template-entity-ref": "template:default/cms-sample",
+ "aws.amazon.com/acdp-assets-ref": "dir:assets",
+ "backstage.io/source-location":
+ "url:https://test-bucket.s3.us-west-2.amazonaws.com/local/backstage/catalog/acdp/component/cms-sample/assets",
+ },
+ description:
+ "A CDK Python app for showing a basic skeleton for a CMS module",
+ name: "cms-sample",
+ namespace: "acdp",
+ },
+ spec: {
+ lifecycle: "experimental",
+ owner: "group:default/asdf",
+ type: "service",
+ },
+};
+
+export const invalidCodeBuildEntity: Entity = {
+ apiVersion: "backstage.io/v1alpha1",
+ kind: "Component",
+ metadata: {
+ uid: "uniqueId",
+ annotations: {
+ "aws.amazon.com/acdp-deploy-on-create": "true",
+ "aws.amazon.com/techdocs-builder": "external",
+ "backstage.io/techdocs-ref": "dir:.",
+ "aws.amazon.com/template-entity-ref": "template:default/cms-sample",
+ "aws.amazon.com/acdp-assets-ref": "dir:assets",
+ "backstage.io/source-location":
+ "url:https://test-bucket.s3.us-west-2.amazonaws.com/local/backstage/catalog/acdp/component/cms-sample/assets",
+ },
+ description:
+ "A CDK Python app for showing a basic skeleton for a CMS module",
+ name: "cms-sample",
+ namespace: "acdp",
+ },
+ spec: {
+ lifecycle: "experimental",
+ owner: "group:default/asdf",
+ type: "service",
+ },
+};
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/plugin.test.ts b/source/modules/acdp/backstage/plugins/acdp/src/plugin.test.ts
new file mode 100644
index 00000000..bf6ecada
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/plugin.test.ts
@@ -0,0 +1,14 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { acdpPlugin, EntityAcdpBuildProjectOverviewCard } from "./plugin";
+
+describe("plugin", () => {
+ it("should export acdp plugin", () => {
+ expect(acdpPlugin).toBeDefined();
+ });
+
+ it("should export acdp CodeBuild Component", () => {
+ expect(EntityAcdpBuildProjectOverviewCard).toBeDefined();
+ });
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/plugin.ts b/source/modules/acdp/backstage/plugins/acdp/src/plugin.ts
new file mode 100644
index 00000000..fcf6c8b8
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/plugin.ts
@@ -0,0 +1,39 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ createApiFactory,
+ createComponentExtension,
+ createPlugin,
+ identityApiRef,
+ configApiRef,
+ BackstagePlugin,
+} from "@backstage/core-plugin-api";
+import { acdpBuildApiRef, AcdpBuildApi } from "./api";
+import { AcdpBuildWidget } from "./components/CodeBuildWidget";
+
+import { rootRouteRef } from "./routes";
+
+export const acdpPlugin: BackstagePlugin = createPlugin({
+ id: "acdp",
+ apis: [
+ createApiFactory({
+ api: acdpBuildApiRef,
+ deps: { configApi: configApiRef, identityApi: identityApiRef },
+ factory: ({ configApi, identityApi }) =>
+ new AcdpBuildApi({ configApi, identityApi }),
+ }),
+ ],
+ routes: {
+ entityContent: rootRouteRef,
+ },
+});
+
+export const EntityAcdpBuildProjectOverviewCard = acdpPlugin.provide(
+ createComponentExtension({
+ name: "EntityAcdpBuildCard",
+ component: {
+ sync: AcdpBuildWidget,
+ },
+ }),
+);
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/routes.ts b/source/modules/acdp/backstage/plugins/acdp/src/routes.ts
new file mode 100644
index 00000000..7c7b6d33
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/routes.ts
@@ -0,0 +1,8 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { createRouteRef } from "@backstage/core-plugin-api";
+
+export const rootRouteRef = createRouteRef({
+ id: "acdp",
+});
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/setupTests.ts b/source/modules/acdp/backstage/plugins/acdp/src/setupTests.ts
new file mode 100644
index 00000000..7ea7f359
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/setupTests.ts
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import "@testing-library/jest-dom";
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/utils/getArnFromEntity.ts b/source/modules/acdp/backstage/plugins/acdp/src/utils/getArnFromEntity.ts
new file mode 100644
index 00000000..3b6219ee
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/utils/getArnFromEntity.ts
@@ -0,0 +1,25 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { validate, parse } from "@aws-sdk/util-arn-parser";
+
+export function parseCodeBuildArn(arn: string): {
+ arn: string;
+ accountId: string;
+ region: string;
+ service: string;
+ resource: string;
+ projectName: string;
+} {
+ if (!validate(arn))
+ throw new Error(`Value for codebuild arn was not a valid ARN: '${arn}'`);
+
+ const parsedArn = parse(arn);
+
+ const resourceParts = parsedArn.resource.split("/");
+
+ if (resourceParts.length !== 2)
+ throw new Error(`CodeBuild ARN not valid: ${arn}`);
+
+ return { projectName: resourceParts[1], arn: arn, ...parsedArn };
+}
diff --git a/source/modules/acdp/backstage/plugins/acdp/src/utils/index.ts b/source/modules/acdp/backstage/plugins/acdp/src/utils/index.ts
new file mode 100644
index 00000000..fc7a4e98
--- /dev/null
+++ b/source/modules/acdp/backstage/plugins/acdp/src/utils/index.ts
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from "./getArnFromEntity";
diff --git a/source/backstage/scripts/plantuml b/source/modules/acdp/backstage/scripts/plantuml
similarity index 100%
rename from source/backstage/scripts/plantuml
rename to source/modules/acdp/backstage/scripts/plantuml
diff --git a/source/backstage/tsconfig.json b/source/modules/acdp/backstage/tsconfig.json
similarity index 86%
rename from source/backstage/tsconfig.json
rename to source/modules/acdp/backstage/tsconfig.json
index ba3f9017..539b1390 100644
--- a/source/backstage/tsconfig.json
+++ b/source/modules/acdp/backstage/tsconfig.json
@@ -6,7 +6,9 @@
"plugins/*/dev",
"plugins/*/migrations"
],
- "exclude": ["node_modules"],
+ "exclude": [
+ "node_modules"
+ ],
"compilerOptions": {
"outDir": "dist-types",
"rootDir": "."
diff --git a/source/modules/acdp/cdk.json b/source/modules/acdp/cdk.json
new file mode 100644
index 00000000..04c7a16e
--- /dev/null
+++ b/source/modules/acdp/cdk.json
@@ -0,0 +1,17 @@
+{
+ "app": "python3 -m source.app",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "requirements*.txt",
+ "source.bat",
+ "python/__pycache__",
+ "tests"
+ ]
+ },
+ "context": {}
+}
diff --git a/source/modules/acdp/deployment/build-s3-dist.sh b/source/modules/acdp/deployment/build-s3-dist.sh
new file mode 100755
index 00000000..e95bcdce
--- /dev/null
+++ b/source/modules/acdp/deployment/build-s3-dist.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+
+showHelp() {
+cat << EOF
+Usage: ./deployment/build-s3-dist.sh --help
+
+Build and synthesize the CFN template. Package the templates into the deployment/global-s3-assets
+folder and the build assets in the deployment/regional-s3-assets folder.
+
+Example:
+make build
+The template will then expect the build assets to be located in the solutions-features-[region_name] bucket.
+EOF
+}
+
+# Get reference for all important folders
+root_dir="$(dirname "$(dirname "$(realpath "$0")")")"
+export MODULE_ROOT_DIR="$root_dir"
+export DEPLOYMENT_DIR="$root_dir/deployment"
+export STAGING_DIST_DIR="$DEPLOYMENT_DIR/staging"
+export GLOBAL_ASSETS_DIR="$DEPLOYMENT_DIR/global-s3-assets"
+export REGIONAL_ASSETS_DIR="$DEPLOYMENT_DIR/regional-s3-assets"
+export LAMBDA_ZIP_OUTPUT_PATH="$root_dir/dist/lambda"
+
+printf "%b[Init] Remove old dist files from previous runs\n%b" "${GREEN}" "${NC}"
+rm -rf "$GLOBAL_ASSETS_DIR"
+rm -rf "$REGIONAL_ASSETS_DIR"
+rm -rf "$STAGING_DIST_DIR"
+rm -rf "$LAMBDA_ZIP_OUTPUT_PATH"
+
+mkdir -p "$GLOBAL_ASSETS_DIR"
+mkdir -p "$REGIONAL_ASSETS_DIR"
+mkdir -p "$STAGING_DIST_DIR"
+mkdir -p "$LAMBDA_ZIP_OUTPUT_PATH"
+
+cd "$root_dir"
+
+../../../deployment/module-build/build-cdk-assets.sh
+
+printf "%bBuild script finished.\n%b" "${GREEN}" "${NC}"
diff --git a/source/modules/acdp/deployment/cdk-solution-helper/README.md b/source/modules/acdp/deployment/cdk-solution-helper/README.md
new file mode 100644
index 00000000..84a4e080
--- /dev/null
+++ b/source/modules/acdp/deployment/cdk-solution-helper/README.md
@@ -0,0 +1,159 @@
+# cdk-solution-helper
+
+A lightweight helper function that cleans-up synthesized templates from the AWS Cloud Development Kit (CDK) and prepares
+them for use with the AWS Solutions publishing pipeline. This function performs the following tasks:
+
+## Lambda function preparation
+
+Replaces the AssetParameter-style properties that identify source code for Lambda functions with the common variables
+used by the AWS Solutions publishing pipeline.
+
+- `Code.S3Bucket` is assigned the `%%DIST_BUCKET_NAME%%` placeholder value.
+- `Code.S3Key` is assigned the `%%SOLUTION_NAME%%`/`%%VERSION%%` placeholder value.
+- `Handler` is given a prefix identical to the artifact hash, enabling the Lambda function to properly find the handler
+in the extracted source code package.
+
+These placeholders are then replaced with the appropriate values using the default find/replace operation run by the pipeline.
+
+Before:
+
+```json
+"examplefunction67F55935": {
+ "Type": "AWS::Lambda::Function",
+ "Properties": {
+ "Code": {
+ "S3Bucket": {
+ "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3Bucket54E71A95"
+ },
+ "S3Key": {
+ "Fn::Join": [
+ "",
+ [
+ {
+ "Fn::Select": [
+ 0,
+ {
+ "Fn::Split": [
+ "||",
+ {
+ "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Fn::Select": [
+ 1,
+ {
+ "Fn::Split": [
+ "||",
+ {
+ "Ref": "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ]
+ }
+ }, ...
+ Handler: "index.handler", ...
+```
+
+After helper function run:
+
+```json
+"examplefunction67F55935": {
+ "Type": "AWS::Lambda::Function",
+ "Properties": {
+ "Code": {
+ "S3Bucket": "%%DIST_BUCKET_NAME%%",
+ "S3Key": "%%SOLUTION_NAME%%/%%VERSION%%/assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip"
+ }, ...
+ "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler"
+```
+
+After build script run:
+
+```json
+"examplefunction67F55935": {
+ "Type": "AWS::Lambda::Function",
+ "Properties": {
+ "Code": {
+ "S3Bucket": "solutions",
+ "S3Key": "trademarked-solution-name/v1.0.0/asset.d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip"
+ }, ...
+ "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler"
+```
+
+After CloudFormation deployment:
+
+```json
+"examplefunction67F55935": {
+ "Type": "AWS::Lambda::Function",
+ "Properties": {
+ "Code": {
+ "S3Bucket": "solutions-us-east-1",
+ "S3Key": "trademarked-solution-name/v1.0.0/asset.d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7.zip"
+ }, ...
+ "Handler": "assetd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7/index.handler"
+```
+
+## Template cleanup
+
+Cleans-up the parameters section and improves readability by removing the AssetParameter-style fields that would have
+been used to specify Lambda source code properties. This allows solution-specific parameters to be highlighted and
+removes unnecessary clutter.
+
+Before:
+
+```json
+"Parameters": {
+ "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3Bucket54E71A95": {
+ "Type": "String",
+ "Description": "S3 bucket for asset \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\""
+ },
+ "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7S3VersionKeyC789D8B1": {
+ "Type": "String",
+ "Description": "S3 key for asset version \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\""
+ },
+ "AssetParametersd513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7ArtifactHash7AA751FE": {
+ "Type": "String",
+ "Description": "Artifact hash for asset \"d513e93e266931de36e1c7e79c27b196f84ab928fce63d364d9152ca501551f7\""
+ },
+ "CorsEnabled" : {
+ "Description" : "Would you like to enable Cross-Origin Resource Sharing (CORS) for the image handler API? Select 'Yes' if so.",
+ "Default" : "No",
+ "Type" : "String",
+ "AllowedValues" : [ "Yes", "No" ]
+ },
+ "CorsOrigin" : {
+ "Description" : "If you selected 'Yes' above, please specify an origin value here. A wildcard (*) value will support any origin.",
+ "Default" : "*",
+ "Type" : "String"
+ }
+ }
+```
+
+After:
+
+```json
+"Parameters": {
+ "CorsEnabled" : {
+ "Description" : "Would you like to enable Cross-Origin Resource Sharing (CORS) for the image handler API? Select 'Yes' if so.",
+ "Default" : "No",
+ "Type" : "String",
+ "AllowedValues" : [ "Yes", "No" ]
+ },
+ "CorsOrigin" : {
+ "Description" : "If you selected 'Yes' above, please specify an origin value here. A wildcard (*) value will support any origin.",
+ "Default" : "*",
+ "Type" : "String"
+ }
+ }
+ ```
+
+***
+© Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
diff --git a/source/modules/acdp/deployment/cdk-solution-helper/index.js b/source/modules/acdp/deployment/cdk-solution-helper/index.js
new file mode 100644
index 00000000..9644dcee
--- /dev/null
+++ b/source/modules/acdp/deployment/cdk-solution-helper/index.js
@@ -0,0 +1,310 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// Imports
+const fs = require("fs");
+
+// Paths
+const globalS3AssetsPath = "../global-s3-assets";
+
+// Substitution constants and functions
+const regionalS3AssetsBucketSub = {
+ "Fn::Join": [
+ "-",
+ [
+ {
+ "Fn::FindInMap": ["Solution", "AssetsConfig", "S3AssetBucketBaseName"],
+ },
+ {
+ "Fn::Sub": "${AWS::Region}",
+ },
+ ],
+ ],
+};
+
+function regionalS3AssetsKeySub(assetPath) {
+ return {
+ "Fn::Join": [
+ "/",
+ [
+ {
+ "Fn::FindInMap": ["Solution", "AssetsConfig", "S3AssetKeyPrefix"],
+ },
+ `${assetPath}`,
+ ],
+ ],
+ };
+}
+
+function substituteLambdaAssets(template, resources) {
+ // Clean-up Lambda function code dependencies
+ const lambdaFunctions = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::Lambda::Function";
+ });
+ lambdaFunctions.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+ if (fn.Properties.hasOwnProperty("Code")) {
+ prop = fn.Properties.Code;
+ } else if (fn.Properties.hasOwnProperty("Content")) {
+ prop = fn.Properties.Content;
+ }
+
+ if (prop.hasOwnProperty("S3Bucket")) {
+ // Set the S3 key reference
+ let artifactHash = Object.assign(prop.S3Key);
+ const assetPath = `asset${artifactHash}`;
+ prop.S3Key = regionalS3AssetsKeySub(assetPath);
+
+ // Set the S3 bucket reference
+ prop.S3Bucket = regionalS3AssetsBucketSub;
+ } else {
+ console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
+ }
+ });
+}
+
+function substituteLambdaLayerAssets(template, resources) {
+ const lambdaLayers = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::Lambda::LayerVersion";
+ });
+ lambdaLayers.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+ if (fn.Properties.hasOwnProperty("Content")) {
+ prop = fn.Properties.Content;
+ }
+
+ if (prop.hasOwnProperty("S3Bucket")) {
+ // Set the S3 key reference
+ let artifactHash = Object.assign(prop.S3Key);
+ const assetPath = `asset${artifactHash}`;
+ prop.S3Key = regionalS3AssetsKeySub(assetPath);
+
+ // Set the S3 bucket reference
+ prop.S3Bucket = regionalS3AssetsBucketSub;
+ } else {
+ console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
+ }
+ });
+}
+
+function substituteServerlessFunctionAssets(template, resources) {
+ const serverlessFunctions = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::Serverless::Function";
+ });
+ serverlessFunctions.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+ if (fn.Properties.hasOwnProperty("CodeUri")) {
+ prop = fn.Properties.CodeUri;
+ }
+
+ if (prop.hasOwnProperty("Bucket")) {
+ // Set the S3 key reference
+ let artifactHash = Object.assign(prop.Key);
+ const assetPath = `asset${artifactHash}`;
+ prop.Key = regionalS3AssetsKeySub(assetPath);
+
+ // Set the S3 bucket reference
+ prop.Bucket = regionalS3AssetsBucketSub;
+ } else {
+ console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
+ }
+ });
+}
+
+function substituteCDKBucketDeploymentAssets(template, resources) {
+ const cdkBucketDeployments = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "Custom::CDKBucketDeployment";
+ });
+ cdkBucketDeployments.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop = fn.Properties;
+
+ if (prop.hasOwnProperty("SourceBucketNames")) {
+ // Set the S3 key reference
+ let artifactHash = Object.assign(prop.SourceObjectKeys);
+ const assetPath = `asset${artifactHash}`;
+ prop.SourceObjectKeys = [regionalS3AssetsKeySub(assetPath)];
+
+ // Set the S3 bucket reference
+ prop.SourceBucketNames = [regionalS3AssetsBucketSub];
+ } else {
+ console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
+ }
+ });
+}
+
+function substituteCodeCommitRepoAssets(template, resources) {
+ const codeCommitRepos = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::CodeCommit::Repository";
+ });
+ codeCommitRepos.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+
+ if (fn.Properties.hasOwnProperty("Code")) {
+ prop = fn.Properties.Code;
+ }
+
+ if (prop.hasOwnProperty("S3")) {
+ prop = prop.S3;
+ // Set the S3 key reference
+ let artifactHash = Object.assign(prop.Key);
+ const assetPath = `asset${artifactHash}`;
+ prop.Key = regionalS3AssetsKeySub(assetPath);
+ // Set the S3 bucket reference
+
+ prop.Bucket = regionalS3AssetsBucketSub;
+ } else {
+ console.warn(`No S3Bucket Property found for ${JSON.stringify(prop)}`);
+ }
+ });
+}
+
+function substituteNestedStackAssets(template, resources) {
+ // Clean-up nested template stack dependencies
+ const nestedStacks = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::CloudFormation::Stack";
+ });
+
+ nestedStacks.forEach(function (f) {
+ const fn = template.Resources[f];
+ let assetPath = fn.Metadata["aws:asset:path"];
+ // get the base name of the asset path file. Trim the .json at the end
+ if (
+ assetPath.substring(assetPath.length - 5, assetPath.length) === ".json"
+ ) {
+ assetPath = assetPath.substring(0, assetPath.length - 5);
+ }
+
+ fn.Properties.TemplateURL = {
+ "Fn::Join": [
+ "",
+ [
+ "https://",
+ regionalS3AssetsBucketSub,
+ ".s3.",
+ {
+ Ref: "AWS::URLSuffix",
+ },
+ "/",
+ regionalS3AssetsKeySub(assetPath),
+ ],
+ ],
+ };
+ });
+}
+
+function compareJsonKeys(json1, json2) {
+ if (typeof json1 !== "object" || typeof json2 !== "object") {
+ return false;
+ }
+ let keys1 = Object.keys(json1).sort();
+ let keys2 = Object.keys(json2).sort();
+
+ return JSON.stringify(keys1) === JSON.stringify(keys2);
+}
+
+function compareJsonsWithRegex(jsonWithPattern, jsonToMatch) {
+ if (
+ typeof jsonWithPattern !== "object" ||
+ typeof jsonToMatch !== "object" ||
+ !compareJsonKeys(jsonWithPattern, jsonToMatch)
+ ) {
+ return false;
+ }
+
+ for (const key in jsonWithPattern) {
+ var re = new RegExp(`^${jsonWithPattern[key]}$`);
+ if (!re.test(jsonToMatch[key])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function replaceSubdict(jsonObj, targetSubdict, replacement) {
+ for (const key in jsonObj) {
+ if (jsonObj[key] && typeof jsonObj[key] === "object") {
+ if (compareJsonsWithRegex(targetSubdict, jsonObj[key])) {
+ jsonObj[key] = { ...replacement };
+ } else {
+ replaceSubdict(jsonObj[key], targetSubdict, replacement);
+ }
+ }
+ }
+}
+
+function substitutePolicies(template, resources) {
+ const policies = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::IAM::Policy";
+ });
+ policies.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+ if (fn.Properties.hasOwnProperty("PolicyDocument")) {
+ prop = fn.Properties.PolicyDocument;
+ }
+
+ let cdkBucketRef = {
+ "Fn::Sub": "cdk-[a-z0-9]+-assets-.*",
+ };
+
+ let customBucketRef = regionalS3AssetsBucketSub;
+
+ replaceSubdict(prop, cdkBucketRef, customBucketRef);
+ });
+}
+
+function substituteRoles(template, resources) {
+ const roles = Object.keys(resources).filter(function (key) {
+ return resources[key].Type === "AWS::IAM::Role";
+ });
+ roles.forEach(function (f) {
+ const fn = template.Resources[f];
+ let prop;
+ if (fn.Properties.hasOwnProperty("Policies")) {
+ prop = fn.Properties.Policies;
+ }
+
+ let cdkBucketRef = {
+ "Fn::Sub": "cdk-[a-z0-9]+-assets-.*",
+ };
+ let customBucketRef = regionalS3AssetsBucketSub;
+
+ replaceSubdict(prop, cdkBucketRef, customBucketRef);
+ });
+}
+
+// For each template in globalS3AssetsPath ...
+fs.readdirSync(globalS3AssetsPath).forEach((file) => {
+ // Import and parse template file
+ const rawTemplate = fs.readFileSync(`${globalS3AssetsPath}/${file}`);
+ let template = JSON.parse(rawTemplate);
+ const resources = template.Resources ? template.Resources : {};
+
+ substituteLambdaAssets(template, resources);
+ substituteLambdaLayerAssets(template, resources);
+ substituteServerlessFunctionAssets(template, resources);
+ substituteCDKBucketDeploymentAssets(template, resources);
+ substituteCodeCommitRepoAssets(template, resources);
+ substituteNestedStackAssets(template, resources);
+ substitutePolicies(template, resources);
+ substituteRoles(template, resources);
+
+ // Clean-up parameters section
+ const parameters = template.Parameters ? template.Parameters : {};
+ const assetParameters = Object.keys(parameters).filter(function (key) {
+ return key.includes("AssetParameters");
+ });
+ assetParameters.forEach(function (a) {
+ template.Parameters[a] = undefined;
+ });
+
+ // Output modified template file
+ const outputTemplate = JSON.stringify(template, null, 2);
+ fs.writeFileSync(`${globalS3AssetsPath}/${file}`, outputTemplate);
+});
diff --git a/templates/modules/cms_alerts_on_aws/v1/instance_infrastructure/deployment/cdk-solution-helper/package.json b/source/modules/acdp/deployment/cdk-solution-helper/package.json
similarity index 100%
rename from templates/modules/cms_alerts_on_aws/v1/instance_infrastructure/deployment/cdk-solution-helper/package.json
rename to source/modules/acdp/deployment/cdk-solution-helper/package.json
diff --git a/source/modules/acdp/deployment/run-backstage-lint.sh b/source/modules/acdp/deployment/run-backstage-lint.sh
new file mode 100755
index 00000000..c6910915
--- /dev/null
+++ b/source/modules/acdp/deployment/run-backstage-lint.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+
+# CD into one level above the deployment dir where this script is located
+# module_root_dir_absolute_path="$PWD"
+cd "$(dirname "$0")"
+cd ../backstage
+
+yarn tsc:full
diff --git a/source/modules/acdp/deployment/run-cfn-nag.sh b/source/modules/acdp/deployment/run-cfn-nag.sh
new file mode 100755
index 00000000..f38d3256
--- /dev/null
+++ b/source/modules/acdp/deployment/run-cfn-nag.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+
+showHelp() {
+cat << EOF
+Usage: ./deployment/run-cfn-nag.sh --help
+
+Run "cdk-nag" and cfn-nag in this module.
+
+-dl, --deny-list-path Pass the file name which contains cfn-nag rules to suppress
+
+EOF
+}
+
+while [[ $# -gt 0 ]]
+do
+key="$1"
+ case $key in
+ -h|--help)
+ showHelp
+ exit 0
+ ;;
+ -dl|--deny-list-path)
+ deny_list_path="$2"
+ shift
+ shift
+ ;;
+ *)
+ shift
+ esac
+done
+
+# CD into one level above the deployment dir where this script is located
+cd "$(dirname "$0")"/..
+
+# Get reference for all important folders
+root_dir="$(dirname "$(dirname "$(realpath "$0")")")"
+deployment_dir="$root_dir/deployment"
+template_dist_dir="$deployment_dir/global-s3-assets"
+
+# Run the build script to build the assets and templates
+printf "%bBuild the assets for the module.%b\n" "${MAGENTA}" "${NC}"
+export CDK_NAG_ENFORCE=true
+make -C "$root_dir" build
+
+did_cfn_nag_fail=0
+# Loop through all files with extension .template in the template_dist_dir
+while IFS= read -r file; do
+ # Check if the file exists and is a file (not a directory)
+ if [[ -f "${file}" ]]; then
+ # Fail if exit code is non-0. The if statement is necessary to prevent exit because of `set -e`.
+ if ! output=$(cfn_nag "${file}" ${deny_list_path:+--deny-list-path=$deny_list_path} 2>&1); then
+ did_cfn_nag_fail=1
+ printf "%bCFN NAG scan failed with failures.%b\n" "${RED}" "${NC}"
+ fi
+ # Check if there are any warnings in the output. cfn_nag does not return a failing exit code on warnings.
+ if [[ "${output}" == *"WARN"* ]]; then
+ did_cfn_nag_fail=1
+ printf "%bCFN NAG scan failed with warnings.%b\n" "${RED}" "${NC}"
+ fi
+ echo "$output"
+ fi
+done < <(find "$template_dist_dir" -name "*.template" -mindepth 1 -type f)
+
+exit $did_cfn_nag_fail
diff --git a/source/modules/acdp/deployment/run-unit-tests.sh b/source/modules/acdp/deployment/run-unit-tests.sh
new file mode 100755
index 00000000..eb3c19c6
--- /dev/null
+++ b/source/modules/acdp/deployment/run-unit-tests.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+
+showHelp() {
+cat << EOF
+Usage: ./deployment/run-unit-tests.sh --help
+
+Run unit tests in this module.
+
+-r, --no-report Don't generate the report, this is mainly used for pre-commit
+
+-s, --snapshot-update Update cdk snapshots
+
+EOF
+}
+
+generate_report=true
+
+for flag in "$@"
+do
+ case "$flag" in
+ -h|--help)
+ showHelp
+ exit 0
+ ;;
+ -r|--no-report)
+ unset generate_report
+ ;;
+ -s|--snapshot-update)
+ snapshot_update=true
+ ;;
+ *)
+ printf "Unrecognized flag %s." "${flag}"
+ printf "Please use --help to see the list of supported flags. This script does not use any positional args.\n"
+ printf "Exiting script with error code 1.\n\n"
+ exit 1
+ ;;
+ esac
+done
+
+cd "$(dirname "$0")"/..
+
+# Get reference for all important folders and files
+project_dir="$PWD"
+source_dir="$project_dir/source"
+backstage_dir="$project_dir/backstage"
+
+tests_dir="$source_dir/tests"
+backstage_cdk_tests_dir="$backstage_dir/cdk/source/tests"
+
+python_coverage_report="$source_dir/tests/coverage-reports/coverage.xml"
+
+rm -f "$project_dir/.coverage"
+
+# Run test on package and save results to coverage_report_path in xml format
+pytest "$tests_dir" "$backstage_cdk_tests_dir" \
+ --cov="$project_dir" \
+ --cov-report=term \
+ --cov-config="$project_dir/pyproject.toml" \
+ ${generate_report:+--cov-report=xml:$python_coverage_report} \
+ ${snapshot_update:+--snapshot-update}
+
+# <=====UNIQUE TO BACKSTAGE=====>
+# Run all ts tests for Backstage
+yarn --cwd "$backstage_dir" test:all
+
+rm -rf "$backstage_dir/coverage/lcov-report"
+# <=====UNIQUE TO BACKSTAGE=====>
+
+# Only perform the sed transformation if a report was generated, to guarantee the coveragereport file exists
+if [ "$generate_report" = true ]
+then
+ # Linux and MacOS have different ways of calling the sed command for in-place editing.
+ # MacOS takes a mandatory argument for the -i flag whereas linux does not.
+ sedi=(-i)
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sedi=(-i "")
+ fi
+
+ # The pytest coverage report generated includes the absolute path to the root directory.
+ # Sonarqube requires a path that is instead relative to the root directory.
+ # To accomplish this, we remove the absolute path portion of the root directory.
+ repo_root="$(dirname "$(dirname "$(dirname "$project_dir")")")"
+ sed "${sedi[@]}" -e "s,$repo_root/,,g" "$python_coverage_report"
+fi
diff --git a/source/modules/acdp/deployment/upload-s3-dist.sh b/source/modules/acdp/deployment/upload-s3-dist.sh
new file mode 100755
index 00000000..d3375df9
--- /dev/null
+++ b/source/modules/acdp/deployment/upload-s3-dist.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+#
+# This script will perform the following tasks:
+# 1. Creates the template and build assets bucket.
+# 2. Copy the contents of the global-s3-assets/ directory to the template bucket
+# 3. Copy the contents of the regional-s3-assets/ directory to the build assets bucket
+#
+# Usage
+# ./deployment/upload-s3-dist.sh
+
+set -e && [[ "$DEBUG" == 'true' ]] && set -x
+shopt -s nullglob
+
+[[ -z "$AWS_ACCOUNT_ID" ]] && printf "%bUnable to identify AWS_ACCOUNT_ID, please add AWS_ACCOUNT_ID to environment variables%b\n" "${RED}" "${NC}" && exit 1
+[[ -z "$AWS_REGION" ]] && printf "%bUnable to identify AWS_REGION, please add AWS_REGION to environment variables%b\n" "${RED}" "${NC}" && exit 1
+[[ -z "$GLOBAL_ASSET_BUCKET_NAME" ]] && printf "%bUnable to identify GLOBAL_ASSET_BUCKET_NAME, please add GLOBAL_ASSET_BUCKET_NAME to environment variables%b\n" "${RED}" "${NC}" && exit 1
+[[ -z "$REGIONAL_ASSET_BUCKET_NAME" ]] && printf "%bUnable to identify REGIONAL_ASSET_BUCKET_NAME, please add REGIONAL_ASSET_BUCKET_NAME to environment variables%b\n" "${RED}" "${NC}" && exit 1
+
+template_dist_dir="$PWD/deployment/global-s3-assets"
+build_dist_dir="$PWD/deployment/regional-s3-assets"
+global_assets_s3_uri="s3://$GLOBAL_ASSET_BUCKET_NAME/$SOLUTION_NAME/$SOLUTION_VERSION"
+regional_assets_s3_uri="s3://$REGIONAL_ASSET_BUCKET_NAME/$SOLUTION_NAME/$SOLUTION_VERSION"
+
+if aws s3api get-bucket-acl --bucket "$GLOBAL_ASSET_BUCKET_NAME" --expected-bucket-owner "$AWS_ACCOUNT_ID" > /dev/null; then
+ printf "%bCopying global-s3-assets to %s...%b\n" "${MAGENTA}" "$global_assets_s3_uri" "${NC}";
+ aws s3 sync "$template_dist_dir" "$global_assets_s3_uri" --quiet;
+else
+ printf "%bBucket ownership verification failed...skipping sync of global assets to %s%b\n" "${MAGENTA}" "$global_assets_s3_uri" "${NC}";
+fi
+
+if aws s3api get-bucket-acl --bucket "$REGIONAL_ASSET_BUCKET_NAME" --expected-bucket-owner "$AWS_ACCOUNT_ID" > /dev/null; then
+ printf "%bCopying regional-s3-assets to %s...%b\n" "${MAGENTA}" "$regional_assets_s3_uri" "${NC}";
+ aws s3 sync "$build_dist_dir" "$regional_assets_s3_uri" --quiet;
+else
+ printf "%bBucket ownership verification failed...skipping sync of global assets to %s%b\n" "${MAGENTA}" "$regional_assets_s3_uri" "${NC}";
+fi
diff --git a/source/modules/acdp/documentation/architecture/cms-acdp-deployment-diagram.svg b/source/modules/acdp/documentation/architecture/cms-acdp-deployment-diagram.svg
new file mode 100644
index 00000000..c9df96ff
--- /dev/null
+++ b/source/modules/acdp/documentation/architecture/cms-acdp-deployment-diagram.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/source/modules/acdp/documentation/postman/postman-acdp-build-api.json b/source/modules/acdp/documentation/postman/postman-acdp-build-api.json
new file mode 100644
index 00000000..b022f1d9
--- /dev/null
+++ b/source/modules/acdp/documentation/postman/postman-acdp-build-api.json
@@ -0,0 +1,181 @@
+{
+ "info": {
+ "_postman_id": "ce2e0058-b302-46de-96f6-49302ed1e9b3",
+ "name": "ACDP Build API Requests",
+ "description": "Collection of ACDP Build API requests",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+ "_exporter_id": "11872555"
+ },
+ "item": [
+ {
+ "name": "Get Project Details",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{Base_URL}}/api/acdp-backend/project?entityRef={{Entity_Ref}}",
+ "host": [
+ "{{Base_URL}}"
+ ],
+ "path": [
+ "api",
+ "acdp-backend",
+ "project"
+ ],
+ "query": [
+ {
+ "key": "entityRef",
+ "value": "{{Entity_Ref}}"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get Builds Details",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "type": "text"
+ }
+ ],
+ "url": {
+ "raw": "{{Base_URL}}/api/acdp-backend/builds?entityRef={{Entity_Ref}}",
+ "host": [
+ "{{Base_URL}}"
+ ],
+ "path": [
+ "api",
+ "acdp-backend",
+ "builds"
+ ],
+ "query": [
+ {
+ "key": "entityRef",
+ "value": "{{Entity_Ref}}"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Start Deploy Build",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "content-type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"entityRef\":\"{{Entity_Ref}}\",\n \"action\": \"deploy\"\n}"
+ },
+ "url": {
+ "raw": "{{Base_URL}}/api/acdp-backend/startBuild",
+ "host": [
+ "{{Base_URL}}"
+ ],
+ "path": [
+ "api",
+ "acdp-backend",
+ "startBuild"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Start Update Build",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "content-type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"entityRef\":\"{{Entity_Ref}}\",\n \"action\": \"update\"\n}"
+ },
+ "url": {
+ "raw": "{{Base_URL}}/api/acdp-backend/startBuild",
+ "host": [
+ "{{Base_URL}}"
+ ],
+ "path": [
+ "api",
+ "acdp-backend",
+ "startBuild"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Start Teardown Build",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "content-type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"entityRef\":\"{{Entity_Ref}}\",\n \"action\": \"teardown\"\n}"
+ },
+ "url": {
+ "raw": "{{Base_URL}}/api/acdp-backend/startBuild",
+ "host": [
+ "{{Base_URL}}"
+ ],
+ "path": [
+ "api",
+ "acdp-backend",
+ "startBuild"
+ ]
+ }
+ },
+ "response": []
+ }
+ ],
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{Access_Token}}",
+ "type": "string"
+ }
+ ]
+ },
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ }
+ ]
+}
diff --git a/source/modules/acdp/documentation/postman/postman-acdp-env.json b/source/modules/acdp/documentation/postman/postman-acdp-env.json
new file mode 100644
index 00000000..826d0066
--- /dev/null
+++ b/source/modules/acdp/documentation/postman/postman-acdp-env.json
@@ -0,0 +1,27 @@
+{
+ "id": "e596ff4a-48c1-49ed-bf4e-15c1eb292207",
+ "name": "ACDP",
+ "values": [
+ {
+ "key": "Access_Token",
+ "value": "",
+ "type": "secret",
+ "enabled": true
+ },
+ {
+ "key": "Base_URL",
+ "value": "https://acdp.endpoint",
+ "type": "default",
+ "enabled": true
+ },
+ {
+ "key": "Entity_Ref",
+ "value": "component:acdp/cms-sample",
+ "type": "default",
+ "enabled": true
+ }
+ ],
+ "_postman_variable_scope": "environment",
+ "_postman_exported_at": "2024-03-08T14:43:13.666Z",
+ "_postman_exported_using": "Postman/10.19.7"
+}
diff --git a/documentation/sequence/cms-acdp-deployment-sequence-diagram.plantuml b/source/modules/acdp/documentation/sequence/cms-acdp-deployment-sequence-diagram.plantuml
similarity index 77%
rename from documentation/sequence/cms-acdp-deployment-sequence-diagram.plantuml
rename to source/modules/acdp/documentation/sequence/cms-acdp-deployment-sequence-diagram.plantuml
index 788c7900..e81e5981 100644
--- a/documentation/sequence/cms-acdp-deployment-sequence-diagram.plantuml
+++ b/source/modules/acdp/documentation/sequence/cms-acdp-deployment-sequence-diagram.plantuml
@@ -10,7 +10,6 @@
!include AWSPuml/Storage/SimpleStorageService.puml
!include AWSPuml/ManagementGovernance/CloudFormation.puml
!include AWSPuml/Containers/ElasticContainerRegistry.puml
-!include AWSPuml/ManagementGovernance/Proton.puml
'Comment out to use default PlantUML sequence formatting
skinparam participant {
@@ -28,9 +27,6 @@ actor User as user
box ACDP Deployment (Step 1)
participant "$CloudFormationIMG()\nCloudFormation" as cfn << CloudFormation >>
-participant "$SimpleStorageServiceIMG()\nBackstage Source Zip" as ps3 << S3 Asset >>
-participant "$LambdaIMG()\nCustom Resource" as cr << Lambda >>
-participant "$ProtonIMG()\nProton" as proton << Proton >>
endbox
box Backstage Deployment Pipeline (Step 2 - Synchronous)
@@ -42,12 +38,6 @@ endbox
'ACDP Deployment
user -> cfn++ #CC2264: deploy Automotive Cloud Developer Portal (ACDP)
-cfn -> ps3++ #3F8624: upload Proton environment tars
-return
-cfn -> cr++ #D86613: create_proton_environment
-cr -> proton++ #CC2264: create Proton environment templates
-return
-return
cfn -> bs3++ #3F8624: create Backstage source zip object
return
cfn -> bcp++ #3355DA: create Backstage pipeline
@@ -63,9 +53,6 @@ activate bcp #3355DA
bcp -> bs3++ #3F8624: get Backstage source
return
bcp -> bcb++ #3355DA: start CodeBuild PipelineProjects
-
-bcb -> bcb: begin Backstage Environment deploy
-cfn <- bcb: deploy Backstage Environment infrastructure
|||
bcb -> bcb: build Backstage docker image
bcb -> ecr++ #F68D05: store Backstage docker image
@@ -74,13 +61,12 @@ bcb -> bcb: begin Backstage deploy
bcb -> ecr++ #F68D05: use Backstage docker image
return
cfn <- bcb: deploy Backstage infrastructure
+activate cfn
|||
+bcp <-- cfn: finish Backstage deploy
+deactivate cfn
bcp <-- bcb:
deactivate bcb
deactivate bcp
-|||
-user <-- cfn: Finish Backstage Environment deploy
-user <-- cfn: Finish Backstage deploy
-deactivate cfn
@enduml
diff --git a/source/modules/acdp/documentation/sequence/cms-acdp-deployment-sequence-diagram.svg b/source/modules/acdp/documentation/sequence/cms-acdp-deployment-sequence-diagram.svg
new file mode 100644
index 00000000..0def3b8c
--- /dev/null
+++ b/source/modules/acdp/documentation/sequence/cms-acdp-deployment-sequence-diagram.svg
@@ -0,0 +1,240 @@
+
\ No newline at end of file
diff --git a/source/modules/acdp/license_header.txt b/source/modules/acdp/license_header.txt
new file mode 100644
index 00000000..03488f70
--- /dev/null
+++ b/source/modules/acdp/license_header.txt
@@ -0,0 +1,2 @@
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
diff --git a/source/modules/acdp/pyproject.toml b/source/modules/acdp/pyproject.toml
new file mode 100644
index 00000000..ae9ea11f
--- /dev/null
+++ b/source/modules/acdp/pyproject.toml
@@ -0,0 +1,79 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta:__legacy__"
+
+[tool.coverage.report]
+fail_under = 80.0
+omit = [
+ "**/deployment/*",
+ "setup.py",
+ "**/tests/*",
+ "source/app.py",
+ "**/*_dependency_layer/**/*"
+]
+
+[tool.isort]
+sections=["FUTURE", "STDLIB", "THIRDPARTY", "AWS", "FIRSTPARTY", "COMMON", "LOCALFOLDER"]
+known_aws=["aws_cdk","aws_lambda_powertools","aws_solutions_constructs","awscrt","awsiot","cdk_nag","chalice","constructs","boto3","botocore"]
+known_common=["cms_common"]
+import_heading_stdlib="Standard Library"
+import_heading_thirdparty="Third Party Libraries"
+import_heading_aws="AWS Libraries"
+import_heading_common="CMS Common Library"
+import_heading_localfolder="Connected Mobility Solution on AWS"
+profile = "black"
+
+[tool.bandit]
+exclude_dirs = ["cdk.out", "build", ".mypy_cache", ".venv", "*/test_*.py", "*/test_*.py"]
+
+[tool.pylint.'SIMILARITIES']
+ # Ignore comments when computing similarities.
+ignore-comments=true
+ # Ignore docstrings when computing similarities.
+ignore-docstrings=true
+ # Ignore imports when computing similarities.
+ignore-imports=true
+ # Minimum lines number of a similarity.
+min-similarity-lines=10
+
+[tool.pylint.'DESIGN']
+ # Maximum number of arguments for function / method.
+max-args=14
+ # Maximum number of attributes for a class (see R0902).
+max-attributes=20
+ # Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+ # Maximum number of branch for function / method body.
+max-branches=12
+ # Maximum number of locals for function / method body.
+max-locals=25
+ # Maximum number of parents for a class (see R0901).
+max-parents=7
+ # Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+ # Maximum number of return / yield for function / method body.
+max-returns=2
+ # Maximum number of statements in function / method body.
+max-statements=60
+ # Minimum number of public methods for a class (see R0903).
+min-public-methods=0
+
+[tool.pylint.'MESSAGES CONTROL']
+# C0114, C0115, C0116 are for docstrings which we don't use
+# W0613 alarms on unused arguments
+# R0801 duplicated code false alarms on IAM statements
+disable = "C0114, C0115, C0116, W0613, R0801"
+
+[tool.pylint.'FORMAT']
+max-line-length=200
+
+[tool.pylint.'TYPECHECK']
+generated-members=["aws_lambda.Runtime"]
+
+[[tool.mypy.overrides]]
+module=["cms_common.*"]
+ignore_missing_imports=true
+
+[[tool.mypy.overrides]]
+module = "moto"
+implicit_reexport = true
diff --git a/source/modules/acdp/setup.py b/source/modules/acdp/setup.py
new file mode 100644
index 00000000..f32ed094
--- /dev/null
+++ b/source/modules/acdp/setup.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Standard Library
+import os
+
+# Third Party Libraries
+import setuptools
+
+try:
+ with open("README.md", "r", encoding="utf-8") as fp:
+ LONG_DESCRIPTION = fp.read()
+except FileNotFoundError:
+ LONG_DESCRIPTION = ""
+
+setuptools.setup(
+ name=os.environ["MODULE_NAME"],
+ version=setuptools.sic(os.environ["MODULE_VERSION"]),
+ description=os.environ["MODULE_DESCRIPTION"],
+ long_description=LONG_DESCRIPTION,
+ long_description_content_type="text/markdown",
+ author=os.environ["MODULE_AUTHOR"],
+ python_requires=f">={os.environ['PYTHON_MINIMUM_VERSION_SUPPORTED']}",
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: JavaScript",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Typing :: Typed",
+ ],
+)
diff --git a/source/modules/acdp/source/.cdk-nag-suppression-list.json b/source/modules/acdp/source/.cdk-nag-suppression-list.json
new file mode 100644
index 00000000..8d09c4c6
--- /dev/null
+++ b/source/modules/acdp/source/.cdk-nag-suppression-list.json
@@ -0,0 +1,371 @@
+{
+ "/acdp/acdp/pipelines-construct/backend-secret/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-SMG4",
+ "reason": "Rotating this type of secret is currently not supported; it will require a simple rotation lambda."
+ }
+ ]
+ },
+ "/acdp/custom-resource-construct/lambda-function/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-L1",
+ "reason": "Some libraries used throughout the solution are not yet supported in Python 3.11. For consistency, all lambdas are currently kept at Python 3.10. Future refactoring of unsupported libraries will enable the use of 3.11 throughout the solution."
+ },
+ {
+ "id": "AwsSolutions-IAM5",
+ "reason": "Log groups have wildcards."
+ }
+ ]
+ },
+ "/acdp/acdp/pipelines-construct/backstage-code-pipeline/ArtifactsBucket/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-S1",
+ "reason": "An artifact bucket does not need S3 bucket for access logs"
+ }
+ ]
+ },
+ "/acdp/acdp/pipelines-construct/cms-vpc-cloudwatch-role/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "appliesTo": [
+ "Resource:::log-stream:*"
+ ],
+ "reason": "Log stream has to be a wildcard"
+ }
+ ]
+ },
+ "/acdp/acdp/pipelines-construct/backstage-build-role/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "appliesTo": [
+ "Action::secretsmanager:*",
+ "Action::ssm:*",
+ "Resource::arn::ssm:::*",
+ "Resource::arn::ssm:::parameter/acdp/config/*",
+ "Resource::arn::secretsmanager:::secret:/acdp/config/*",
+ "Resource::arn::secretsmanager:::secret:solution//*",
+ "Resource::arn::ssm:::parameter:solution//*"
+ ],
+ "reason": "Pipeline creates and reads multiple secrets and SSM parameters."
+ }
+ ]
+ },
+ "/acdp/acdp/pipelines-construct/backstage-deploy-role/Resource": {
+ "rules_to_suppress": [
+ {
+ "id": "AwsSolutions-IAM5",
+ "appliesTo": [
+ "Resource::arn::ssm:::parameter/acdp/config/*",
+ "Resource::arn::s3:::{\"Fn::FindInMap\":[\"Solution\",\"AssetsConfig\",\"S3AssetBucketBaseName\"]}-/*",
+ "Resource::arn::cloudformation:::stack/cms-*",
+ "Resource::arn::cloudformation:::stack/acdp-backstage-*",
+ "Resource::arn::ssm:::parameter/dev/acdp-dev/*",
+ "Resource::*",
+ "Resource::arn::cloudformation::