From a5036de17fbfae9de6e3cc489e87f188c0e835a1 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Thu, 9 Oct 2025 00:41:18 -0600 Subject: [PATCH 01/21] adding access denied page when required "user" group is set and user isn't an admin or user --- lib/user-interface/react/src/App.tsx | 19 +++++++++++++------ .../react/src/components/app-configured.tsx | 1 + .../react/src/shared/model/user.model.ts | 1 + .../react/src/shared/reducers/user.reducer.ts | 1 + 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/user-interface/react/src/App.tsx b/lib/user-interface/react/src/App.tsx index 1edd43a66..56356e559 100644 --- a/lib/user-interface/react/src/App.tsx +++ b/lib/user-interface/react/src/App.tsx @@ -26,7 +26,7 @@ import Chatbot from './pages/Chatbot'; import Topbar from './components/Topbar'; import SystemBanner from './components/system-banner/system-banner'; import { useAppSelector } from './config/store'; -import { selectCurrentUserIsAdmin } from './shared/reducers/user.reducer'; +import { selectCurrentUserIsAdmin, selectCurrentUserIsUser } from './shared/reducers/user.reducer'; import ModelManagement from './pages/ModelManagement'; import ModelLibrary from './pages/ModelLibrary'; import NotificationBanner from './shared/notification/notification'; @@ -51,16 +51,23 @@ export type RouteProps = { configs?: IConfiguration }; -const PrivateRoute = ({ children, showConfig, configs }: RouteProps) => { +const PrivateRoute = ({ children }: RouteProps) => { const auth = useAuth(); - if (auth.isAuthenticated) { - if (showConfig && configs?.configuration.enabledComponents[showConfig] === false) { - return ; - } + const isUserAdmin = useAppSelector(selectCurrentUserIsAdmin); + const isUser = useAppSelector(selectCurrentUserIsUser); + + if (auth.isAuthenticated && (isUserAdmin || isUser)) { return children; } else if (auth.isLoading) { return ; + } else if (auth.isAuthenticated && !isUserAdmin && !isUser) { + return ( +
+

Access Denied

+

You do not have permission to access this application. Please contact your administrator.

+
+ ); } else { return ; } diff --git a/lib/user-interface/react/src/components/app-configured.tsx b/lib/user-interface/react/src/components/app-configured.tsx index c4eb4d382..fd7d314b6 100644 --- a/lib/user-interface/react/src/components/app-configured.tsx +++ b/lib/user-interface/react/src/components/app-configured.tsx @@ -60,6 +60,7 @@ function AppConfigured () { email: oidcUser.profile.email, groups: userGroups, isAdmin: userGroups ? isAdmin(userGroups) : false, + isUser: window.env.USER_GROUP ? userGroups && isUser(userGroups) : true, }), ); } diff --git a/lib/user-interface/react/src/shared/model/user.model.ts b/lib/user-interface/react/src/shared/model/user.model.ts index 530135b2e..8840462ca 100644 --- a/lib/user-interface/react/src/shared/model/user.model.ts +++ b/lib/user-interface/react/src/shared/model/user.model.ts @@ -19,5 +19,6 @@ export type IUser = { preferred_username: string; email: string; isAdmin: boolean; + isUser: boolean; groups?: string[]; }; diff --git a/lib/user-interface/react/src/shared/reducers/user.reducer.ts b/lib/user-interface/react/src/shared/reducers/user.reducer.ts index 6ff9b12ec..829430eaf 100644 --- a/lib/user-interface/react/src/shared/reducers/user.reducer.ts +++ b/lib/user-interface/react/src/shared/reducers/user.reducer.ts @@ -33,6 +33,7 @@ export const User = createSlice({ }); export const selectCurrentUserIsAdmin = (state: any) => state.user.info?.isAdmin ?? false; +export const selectCurrentUserIsUser = (state: any) => state.user.info?.isUser ?? false; export const selectCurrentUsername = (state: any) => state.user.info?.preferred_username ?? ''; export const selectCurrentUserGroups = (state: any) => state.user.info?.groups ?? []; From d101c5c1c5ba748951e9a8ee4dbd82d9908d445d Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Thu, 9 Oct 2025 09:29:52 -0600 Subject: [PATCH 02/21] Don't return VPC gateways when looking one up --- ecs_model_deployer/src/lib/lisa_model_stack.ts | 3 ++- lib/networking/vpc/index.ts | 1 + lib/schema/configSchema.ts | 2 +- vector_store_deployer/src/lib/opensearch.ts | 3 ++- vector_store_deployer/src/lib/pgvector.ts | 3 ++- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ecs_model_deployer/src/lib/lisa_model_stack.ts b/ecs_model_deployer/src/lib/lisa_model_stack.ts index ab4eab304..402e2fdf8 100644 --- a/ecs_model_deployer/src/lib/lisa_model_stack.ts +++ b/ecs_model_deployer/src/lib/lisa_model_stack.ts @@ -57,7 +57,8 @@ export class LisaModelStack extends Stack { super(scope, id, props); const vpc = Vpc.fromLookup(this, `${id}-vpc`, { - vpcId: props.vpcId + vpcId: props.vpcId, + returnVpnGateways: false, }); let subnetSelection: SubnetSelection | undefined; diff --git a/lib/networking/vpc/index.ts b/lib/networking/vpc/index.ts index 2341e6cf3..97e40b52d 100644 --- a/lib/networking/vpc/index.ts +++ b/lib/networking/vpc/index.ts @@ -67,6 +67,7 @@ export class Vpc extends Construct { // Imports VPC for use by application if supplied, else creates a VPC. vpc = ec2Vpc.fromLookup(this, 'imported-vpc', { vpcId: config.vpcId, + returnVpnGateways: false, }); // Checks if SubnetIds are provided in the config, if so we import them for use. diff --git a/lib/schema/configSchema.ts b/lib/schema/configSchema.ts index 29804e78e..dbf54504b 100644 --- a/lib/schema/configSchema.ts +++ b/lib/schema/configSchema.ts @@ -898,7 +898,7 @@ export const RawConfigObject = z.object({ .describe('Aspect CDK injector for permissions. Ref: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.PermissionsBoundary.html'), stackSynthesizer: z.nativeEnum(stackSynthesizerType).optional().describe('Set the stack synthesize type. Ref: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.StackSynthesizer.html'), bootstrapQualifier: z.string().optional().describe('CDK bootstrap qualifier to use for stack synthesis. Defaults to CDK default if not specified.'), - bootstrapRolePrefix: z.string().optional().describe('Prefix for CDK bootstrap role names. Useful when roles have custom prefixes like LLNL_User_Roles_. Leave empty for standard role names.'), + bootstrapRolePrefix: z.string().optional().describe('Prefix for CDK bootstrap role names. Useful when roles have custom prefixes like My_User_Roles_. Leave empty for standard role names.'), litellmConfig: LiteLLMConfig, convertInlinePoliciesToManaged: z.boolean().optional().default(false).describe('Convert inline policies to managed policies'), iamRdsAuth: z.boolean().optional().default(false).describe('Enable IAM authentication for RDS'), diff --git a/vector_store_deployer/src/lib/opensearch.ts b/vector_store_deployer/src/lib/opensearch.ts index a62b4ae47..aa8cc0239 100644 --- a/vector_store_deployer/src/lib/opensearch.ts +++ b/vector_store_deployer/src/lib/opensearch.ts @@ -49,7 +49,8 @@ export class OpenSearchVectorStoreStack extends PipelineStack { } const vpc = Vpc.fromLookup(this, 'Vpc', { - vpcId + vpcId, + returnVpnGateways: false, }); let subnetSelection: SubnetSelection | undefined; diff --git a/vector_store_deployer/src/lib/pgvector.ts b/vector_store_deployer/src/lib/pgvector.ts index 1c2e318db..b48ac32d2 100644 --- a/vector_store_deployer/src/lib/pgvector.ts +++ b/vector_store_deployer/src/lib/pgvector.ts @@ -59,7 +59,8 @@ export class PGVectorStoreStack extends PipelineStack { // Lookup VPC with given vpcId const vpc = Vpc.fromLookup(this, 'Vpc', { - vpcId + vpcId, + returnVpnGateways: false, }); // Optional subnet selection based on provided subnets From 996485a543272ccb2899d5c487f3067cb7891e85 Mon Sep 17 00:00:00 2001 From: Dustin Sweigart Date: Tue, 14 Oct 2025 12:59:28 -0400 Subject: [PATCH 03/21] Add Nix flake for development environment configuration (#506) --- .envrc | 1 + flake.lock | 61 ++++++++++++++++++++++++ flake.nix | 108 +++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 29 +----------- requirements-dev.txt | 1 - 5 files changed, 171 insertions(+), 29 deletions(-) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..3550a30f2 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..5cc6d190c --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1760284886, + "narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..5c018f6a0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,108 @@ +{ + # LISA Development Environment Flake + # + # LISA (LLM Inference Solution for Amazon) is an open source infrastructure-as-code + # solution for deploying LLM inference capabilities into AWS accounts. This flake + # provides a complete development environment with all necessary tools and dependencies + # for developing, testing, and deploying LISA. + description = "Development environment for LISA - LLM Inference Solution for Amazon"; + + inputs = { + # Use the unstable channel for latest package versions + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + # Utility functions for creating flakes that work across multiple systems + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + # Generate outputs for all default systems (x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin) + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + # Formatter for this flake (run with `nix fmt`) + formatter = pkgs.nixpkgs-fmt; + + # Default development shell (enter with `nix develop`) + devShells.default = pkgs.mkShell { + # Core development tools needed for LISA + packages = with pkgs; [ + awscli2 # AWS command-line interface for deployment and management + jq # JSON processor for parsing AWS responses and configuration + pre-commit # Git hook framework for code quality checks + python3 # Python runtime for LISA backend services + nodejs # Node.js runtime for CDK infrastructure and frontend tooling + uv # Fast Python package installer and virtual environment manager + yq # YAML processor for configuration management + ]; + + # Script that runs when entering the development shell + shellHook = '' + echo "Welcome to the LISA development environment!" + echo "Python: $(python --version)" + echo "Node: $(node --version)" + echo "" + + # Set up Python virtual environment using uv + if [ ! -d .venv ]; then + echo "Creating Python virtual environment with uv..." + uv venv + else + echo "Using existing Python virtual environment." + fi + + # Ensure we start fresh if another venv is active + if [ -n "$VIRTUAL_ENV" ]; then + echo "Deactivating existing virtual environment..." + deactivate + fi + + # Activate the project virtual environment + source .venv/bin/activate + + # Ensure pip is up to date + uv pip install --upgrade pip + + # Initialize npm project if package.json doesn't exist + if [ ! -f package.json ]; then + echo "No package.json found. Running npm init..." + npm init -y + fi + + # Install Python development dependencies + echo "Installing Python development dependencies from requirements-dev.txt..." + + # Extract packages that must be installed as binary wheels (no source builds) + only_binary_packages=`grep "^--only-binary=" requirements-dev.txt | sed 's/^--only-binary=//' | tr ',' ' ' | tr -s ' ' | cut -d' ' -f1-` + echo "Extracted binary-only packages: $only_binary_packages" + + # Install requirements with --only-binary flags converted to command-line arguments + # This removes the --only-binary line from the file and passes it as CLI args instead + echo "Installing filtered requirements-dev.txt..." + uv pip install -r <(sed '/^--only-binary/d' requirements-dev.txt) `for p in "$$=only_binary_packages"; do echo "--only-binary=$$p"; done` + + # Install LISA SDK in editable mode with binary-only installation + echo "Installing lisa-sdk in editable mode..." + uv pip install --only-binary :all: -e lisa-sdk + + # Install Node.js dependencies + echo "Installing Node.js dependencies..." + npm install + + # Configure git hooks for pre-commit + # Unset any existing hooks path to ensure pre-commit can manage hooks + git config --unset-all core.hooksPath 2>/dev/null || true + pre-commit install + + echo "" + echo "Development environment ready!" + echo "Available commands:" + echo " uv pip - For faster package management" + echo " deploylisa - Clean build and deploy LISA in headless mode" + ''; + }; + } + ); +} diff --git a/package-lock.json b/package-lock.json index 5b3031e52..f85ce83b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -292,19 +292,6 @@ } } }, - "lib/user-interface/react/node_modules/yaml": { - "version": "2.8.1", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/@algolia/abtesting": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.5.0.tgz", @@ -19541,20 +19528,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -21577,7 +21550,7 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">= 6" diff --git a/requirements-dev.txt b/requirements-dev.txt index 4ac719d83..c9d32fece 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,7 +20,6 @@ langchain-openai==0.3.33 langchain-text-splitters==0.3.11 cachetools==5.5.0 --only-binary=pyarrow,lxml,psycopg2-binary -pyarrow==17.0.0 # Testing pytest==8.3.2 From b5603ad5925bbff154ba78cdad7e5caf0c689d15 Mon Sep 17 00:00:00 2001 From: Andrew Batzel Date: Tue, 14 Oct 2025 14:49:41 -0600 Subject: [PATCH 04/21] gpt oss compatability --- ecs_model_deployer/src/lib/ecsCluster.ts | 3 ++- eslint.config.js => eslint.config.mjs | 0 lambda/models/state_machine/create_model.py | 2 +- lib/docs/index.ts | 2 +- lib/schema/configSchema.ts | 3 ++- lib/serve/ecs-model/vllm/src/entrypoint.sh | 17 +++++++++++++++++ package.json | 3 ++- 7 files changed, 25 insertions(+), 5 deletions(-) rename eslint.config.js => eslint.config.mjs (100%) diff --git a/ecs_model_deployer/src/lib/ecsCluster.ts b/ecs_model_deployer/src/lib/ecsCluster.ts index 280676e6a..1618d38dd 100644 --- a/ecs_model_deployer/src/lib/ecsCluster.ts +++ b/ecs_model_deployer/src/lib/ecsCluster.ts @@ -237,7 +237,8 @@ export class ECSCluster extends Construct { environment, logging: LogDriver.awsLogs({ streamPrefix: identifier }), gpuCount: Ec2Metadata.get(ecsConfig.instanceType).gpuCount, - memoryReservationMiB: Ec2Metadata.get(ecsConfig.instanceType).memory - ecsConfig.containerMemoryBuffer, + memoryReservationMiB: taskDefinition.containerConfig.memoryReservation ?? + (Ec2Metadata.get(ecsConfig.instanceType).memory - ecsConfig.containerMemoryBuffer), portMappings: [{ hostPort: 80, containerPort: 8080, protocol: Protocol.TCP }], healthCheck: containerHealthCheck, // Model containers need to run with privileged set to true diff --git a/eslint.config.js b/eslint.config.mjs similarity index 100% rename from eslint.config.js rename to eslint.config.mjs diff --git a/lambda/models/state_machine/create_model.py b/lambda/models/state_machine/create_model.py index b757c0238..f70163b7b 100644 --- a/lambda/models/state_machine/create_model.py +++ b/lambda/models/state_machine/create_model.py @@ -137,7 +137,7 @@ def handle_start_copy_docker_image(event: Dict[str, Any], context: Any) -> Dict[ # Remove registry URL if present to get just the repository name if "/" in repository_name: - repository_name = repository_name.split("/")[-1] + repository_name = repository_name.split("/", 1)[1] # Verify image exists in ECR ecrClient.describe_images(repositoryName=repository_name, imageIds=[{"imageTag": image_tag}]) diff --git a/lib/docs/index.ts b/lib/docs/index.ts index ae9dabd50..09822c3c3 100644 --- a/lib/docs/index.ts +++ b/lib/docs/index.ts @@ -17,7 +17,7 @@ import { Construct } from 'constructs'; import { LisaDocsConstruct, LisaDocsProps } from './docConstruct'; import { Stack } from 'aws-cdk-lib'; -export * from './docConstruct'; +export { LisaDocsConstruct, LisaDocsProps }; /** * Lisa Docs Stack diff --git a/lib/schema/configSchema.ts b/lib/schema/configSchema.ts index dbf54504b..cdb72c6ec 100644 --- a/lib/schema/configSchema.ts +++ b/lib/schema/configSchema.ts @@ -461,7 +461,8 @@ export const ContainerConfigSchema = z.object({ .describe('Environment variables for the container.'), sharedMemorySize: z.number().min(0).default(0).describe('The value for the size of the /dev/shm volume.'), healthCheckConfig: ContainerHealthCheckConfigSchema.default({}), - privileged: z.boolean().optional() + privileged: z.boolean().optional(), + memoryReservation: z.number().min(0).optional().describe('Memory reservation in MiB for the container.') }).describe('Configuration for the container.'); export type ContainerConfig = z.infer; diff --git a/lib/serve/ecs-model/vllm/src/entrypoint.sh b/lib/serve/ecs-model/vllm/src/entrypoint.sh index 4a5492aa8..472a4d6da 100644 --- a/lib/serve/ecs-model/vllm/src/entrypoint.sh +++ b/lib/serve/ecs-model/vllm/src/entrypoint.sh @@ -27,6 +27,23 @@ if [[ -n "${MAX_TOTAL_TOKENS}" ]]; then ADDITIONAL_ARGS+=" --max-model-len ${MAX_TOTAL_TOKENS}" fi +# Add vLLM specific arguments from environment variables +if [[ -n "${VLLM_TENSOR_PARALLEL_SIZE}" ]]; then + ADDITIONAL_ARGS+=" --tensor-parallel-size ${VLLM_TENSOR_PARALLEL_SIZE}" +fi + +if [[ -n "${VLLM_ASYNC_SCHEDULING}" ]] && [[ "${VLLM_ASYNC_SCHEDULING}" == "true" ]]; then + ADDITIONAL_ARGS+=" --async-scheduling" +fi + +if [[ -n "${VLLM_MAX_PARALLEL_LOADING_WORKERS}" ]]; then + ADDITIONAL_ARGS+=" --max-parallel-loading-workers ${VLLM_MAX_PARALLEL_LOADING_WORKERS}" +fi + +if [[ -n "${VLLM_USE_TQDM_ON_LOAD}" ]] && [[ "${VLLM_USE_TQDM_ON_LOAD}" == "true" ]]; then + ADDITIONAL_ARGS+=" --use-tqdm-on-load" +fi + # Start the webserver echo "Starting vLLM" python3 -m vllm.entrypoints.openai.api_server \ diff --git a/package.json b/package.json index ac79c2100..9b5fc57be 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,8 @@ }, "ts-node": { "esm": true, - "experimentalSpecifierResolution": "node" + "experimentalSpecifierResolution": "node", + "experimentalSpecifiers": true }, "main": "jest.config.js", "directories": { From 3ebc5cfb37eaf242155b89ec3348f6aff830f637 Mon Sep 17 00:00:00 2001 From: Bear Danley Date: Wed, 15 Oct 2025 20:15:27 +0000 Subject: [PATCH 05/21] Add mcp image override --- lib/api-base/fastApiContainer.ts | 11 ++++++----- lib/schema/configSchema.ts | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/api-base/fastApiContainer.ts b/lib/api-base/fastApiContainer.ts index 3f2a533df..f4cf4e70a 100644 --- a/lib/api-base/fastApiContainer.ts +++ b/lib/api-base/fastApiContainer.ts @@ -125,6 +125,11 @@ export class FastApiContainer extends Construct { path: REST_API_PATH, type: EcsSourceType.ASSET }; + const mcpWorkbenchImage = config.mcpWorkbenchConfig || { + baseImage: config.baseImage, + path: MCP_WORKBENCH_PATH, + type: EcsSourceType.ASSET + }; const instanceType = 'm5.large'; const healthCheckConfig = { command: ['CMD-SHELL', 'exit 0'], @@ -167,11 +172,7 @@ export class FastApiContainer extends Construct { MCPWORKBENCH_BUCKET: [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(), }, containerConfig: { - image: { - baseImage: config.baseImage, - path: MCP_WORKBENCH_PATH, - type: EcsSourceType.ASSET - }, + image: mcpWorkbenchImage, healthCheckConfig, environment: {}, sharedMemorySize: 0, diff --git a/lib/schema/configSchema.ts b/lib/schema/configSchema.ts index cdb72c6ec..c499c1053 100644 --- a/lib/schema/configSchema.ts +++ b/lib/schema/configSchema.ts @@ -814,6 +814,7 @@ export const RawConfigObject = z.object({ partition: z.string().default('aws').describe('AWS partition for deployment.'), domain: z.string().default('amazonaws.com').describe('AWS domain for deployment'), restApiConfig: FastApiContainerConfigSchema.describe('Image override for Rest API'), + mcpWorkbenchConfig: ImageAssetSchema.optional().describe('Image override for MCP Workbench'), batchIngestionConfig: ImageAssetSchema.optional().describe('Image override for Batch Ingestion'), vpcId: z.string().optional().describe('VPC ID for the application. (e.g. vpc-0123456789abcdef)'), subnets: z.array(z.object({ From 18bc5e2f393a1559b03f535dda5dbdfcb27c5db8 Mon Sep 17 00:00:00 2001 From: bedanley Date: Fri, 17 Oct 2025 14:00:55 -0600 Subject: [PATCH 06/21] Allow MCP Image overrides --- bin/build-images | 6 ++++ lib/api-base/ecsCluster.ts | 47 +++++++++------------------ lib/core/iam/ecs.json | 11 ------- lib/core/iam/roles.ts | 4 +++ lib/docs/config/role-overrides.md | 46 ++++++++++++++++++++++++++ lib/iam/iamConstruct.ts | 36 ++++++++++++++------ lib/schema/configSchema.ts | 2 ++ lib/serve/mcp-workbench/Dockerfile | 3 +- test/cdk/mocks/roles.yaml | 8 ++++- test/cdk/stacks/roleOverrides.test.ts | 4 +-- 10 files changed, 110 insertions(+), 57 deletions(-) diff --git a/bin/build-images b/bin/build-images index ae9377f21..52997729f 100755 --- a/bin/build-images +++ b/bin/build-images @@ -117,6 +117,12 @@ build_all_images() { rsync -av --exclude='__pycache__' ./lambda/ "$BUILD_DIR/" build_image "Dockerfile" "lisa-batch-ingestion" "$LISA_VERSION" "$RAG_DIR" "NODE_ENV=production" + # lisa-mcp-workbench + MCP_DIR="./lib/serve/mcp-workbench" + build_image "Dockerfile" "lisa-mcp-workbench" "$LISA_VERSION" "$MCP_DIR" \ + "NODE_ENV=production" \ + "BASE_IMAGE=python:3.13.7-slim" + # lisa-tei build_image "Dockerfile" "lisa-tei" "latest" "./lib/serve/ecs-model/embedding/tei" \ "NODE_ENV=production" \ diff --git a/lib/api-base/ecsCluster.ts b/lib/api-base/ecsCluster.ts index 225002489..73a10908c 100644 --- a/lib/api-base/ecsCluster.ts +++ b/lib/api-base/ecsCluster.ts @@ -45,7 +45,7 @@ import { ListenerCondition, SslPolicy, } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; -import { Effect, IRole, ManagedPolicy, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; +import { IRole, ManagedPolicy, Role } from 'aws-cdk-lib/aws-iam'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; @@ -124,24 +124,9 @@ export class ECSCluster extends Construct { ...(executionRole && { executionRole }), }); - // Grant CloudWatch logs permissions to both task role and execution role + // Grant CloudWatch logs write permissions to task role and execution role logGroup.grantWrite(taskRole); - if (executionRole) { - logGroup.grantWrite(executionRole); - } else { - // If no custom execution role, ensure the default execution role has CloudWatch permissions - // This is critical for log stream creation during container startup - ec2TaskDefinition.addToExecutionRolePolicy(new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'logs:CreateLogGroup', - 'logs:CreateLogStream', - 'logs:PutLogEvents', - 'logs:DescribeLogStreams' - ], - resources: [logGroup.logGroupArn, `${logGroup.logGroupArn}:*`] - })); - } + logGroup.grantWrite(ec2TaskDefinition.obtainExecutionRole()); // Add container to task definition const containerHealthCheckConfig = taskDefinition.containerConfig.healthCheckConfig; @@ -193,20 +178,6 @@ export class ECSCluster extends Construct { super(scope, id); const { config, identifier, vpc, securityGroup, ecsConfig } = props; - // Retrieve execution role if it has been overridden - const executionRole = config.roles ? Role.fromRoleArn( - this, - createCdkId([identifier, 'ER']), - StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/${identifier}EX`), - ) : undefined; - - // Create ECS task definition - const taskRole = Role.fromRoleArn( - this, - createCdkId([identifier, 'TR']), - StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/${identifier}`), - ); - // Create ECS cluster const cluster = new Cluster(this, createCdkId([config.deploymentName, config.deploymentStage, 'Cl']), { clusterName: createCdkId([config.deploymentName, config.deploymentStage, identifier], 32, 2), @@ -414,6 +385,18 @@ export class ECSCluster extends Construct { .join(','); Object.entries(ecsConfig.tasks).forEach(([name, definition]) => { + // Retrieve task role and execution role for each task + const taskRole = Role.fromRoleArn( + this, + createCdkId([name, 'TR']), + StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/${name}`), + ); + const executionRole = Role.fromRoleArn( + this, + createCdkId([name, 'ER']), + StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/${name}EX`), + ); + const taskResult = this.createTaskDefinition( name, config, diff --git a/lib/core/iam/ecs.json b/lib/core/iam/ecs.json index 38b59a068..8f35ae796 100644 --- a/lib/core/iam/ecs.json +++ b/lib/core/iam/ecs.json @@ -92,17 +92,6 @@ ], "Resource": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*" }, - { - "Effect": "Allow", - "Action": [ - "logs:DescribeLogGroups", - "logs:CreateLogStream", - "logs:FilterLogEvents", - "logs:PutLogEvents", - "logs:DescribeLogStreams" - ], - "Resource": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*" - }, { "Effect": "Allow", "Action": [ diff --git a/lib/core/iam/roles.ts b/lib/core/iam/roles.ts index 8118a4424..34788c7d9 100644 --- a/lib/core/iam/roles.ts +++ b/lib/core/iam/roles.ts @@ -28,7 +28,9 @@ export enum Roles { ECS_MODEL_DEPLOYER_ROLE = 'ECSModelDeployerRole', ECS_MODEL_TASK_ROLE = 'ECSModelTaskRole', ECS_REST_API_EX_ROLE = 'ECSRestApiExRole', + ECS_MCPWORKBENCH_API_EX_ROLE = 'ECSMcpWorkbenchApiExRole', ECS_REST_API_ROLE = 'ECSRestApiRole', + ECS_MCPWORKBENCH_API_ROLE = 'ECSMcpWorkbenchApiRole', LAMBDA_CONFIGURATION_API_EXECUTION_ROLE = 'LambdaConfigurationApiExecutionRole', LAMBDA_EXECUTION_ROLE = 'LambdaExecutionRole', MODEL_API_ROLE = 'ModelApiRole', @@ -53,7 +55,9 @@ export const RoleNames: Record = { [Roles.ECS_MODEL_DEPLOYER_ROLE]: 'ECSModelDeployerRole', [Roles.ECS_MODEL_TASK_ROLE]: 'ECSModelTaskRole', [Roles.ECS_REST_API_EX_ROLE]: 'ECSRestApiExRole', + [Roles.ECS_MCPWORKBENCH_API_EX_ROLE]: 'ECSMcpWorkbenchApiExRole', [Roles.ECS_REST_API_ROLE]: 'ECSRestApiRole', + [Roles.ECS_MCPWORKBENCH_API_ROLE]: 'ECSMcpWorkbenchApiRole', [Roles.LAMBDA_CONFIGURATION_API_EXECUTION_ROLE]: 'LambdaConfigurationApiExecutionRole', [Roles.LAMBDA_EXECUTION_ROLE]: 'LambdaExecutionRole', [Roles.MODEL_API_ROLE]: 'ModelApiRole', diff --git a/lib/docs/config/role-overrides.md b/lib/docs/config/role-overrides.md index 6eafa8c6f..e3293542e 100644 --- a/lib/docs/config/role-overrides.md +++ b/lib/docs/config/role-overrides.md @@ -123,6 +123,28 @@ The example provided is an export from a deployed LISA instance based on Least P } } }, + "ECSMcpWorkbenchApiExRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + } + }, "ECSRestApiExRoleDefaultPolicy": { "Type": "AWS::IAM::Policy", "Properties": { @@ -736,6 +758,30 @@ The example provided is an export from a deployed LISA instance based on Least P "RoleName": "app-REST-Role" } }, + "ECSMcpWorkbenchApiRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Allow MCP Workbench API task access to AWS resources", + "ManagedPolicyArns": [ + { + "Ref": "appECSPolicy361D8A62" + } + ], + "RoleName": "app-REST-Role" + } + }, "DocsDeployerRole": { "Type": "AWS::IAM::Role", "Properties": { diff --git a/lib/iam/iamConstruct.ts b/lib/iam/iamConstruct.ts index 82a7707c7..00eac6060 100644 --- a/lib/iam/iamConstruct.ts +++ b/lib/iam/iamConstruct.ts @@ -78,6 +78,10 @@ export class LisaServeIAMSConstruct extends Construct { id: 'REST', type: ECSTaskType.API, }, + { + id: 'MCPWORKBENCH', + type: ECSTaskType.API, + }, ]; ecsRoles.forEach((role) => { @@ -95,17 +99,18 @@ export class LisaServeIAMSConstruct extends Construct { description: `Role ARN for LISA ${role.type} ${role.id} ECS Task`, }); - if (config.roles) { - const executionRoleOverride = getRoleId(`ECS_${role.id}_${role.type}_EX_ROLE`.toUpperCase()); + const executionRoleId = createCdkId([role.id, 'ExRole']); + const executionRoleName = createCdkId([config.deploymentName, role.id, 'ExRole']); + const executionRole = config.roles ? ( // @ts-expect-error - dynamic key lookup of object - const executionRole = Role.fromRoleName(scope, createCdkId([role.id, 'ExRole']), config.roles[executionRoleOverride]); - - new StringParameter(scope, createCdkId([config.deploymentName, role.id, 'EX', 'SP']), { - parameterName: `${config.deploymentPrefix}/roles/${role.id}EX`, - stringValue: executionRole.roleArn, - description: `Role ARN for LISA ${role.type} ${role.id} ECS Execution`, - }); - } + Role.fromRoleName(scope, executionRoleId, config.roles[getRoleId(`ECS_${role.id}_${role.type}_EX_ROLE`.toUpperCase())]) + ) : this.createEcsExecutionRole(role, executionRoleId, executionRoleName); + + new StringParameter(scope, createCdkId([config.deploymentName, role.id, 'EX', 'SP']), { + parameterName: `${config.deploymentPrefix}/roles/${role.id}EX`, + stringValue: executionRole.roleArn, + description: `Role ARN for LISA ${role.type} ${role.id} ECS Execution`, + }); }); } @@ -175,4 +180,15 @@ export class LisaServeIAMSConstruct extends Construct { managedPolicies: [taskPolicy], }); } + + private createEcsExecutionRole (role: ECSRole, roleId: string, roleName: string): IRole { + return new Role(this.scope, roleId, { + assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), + roleName, + description: `Allow ${role.id} ${role.type} execution role to pull images and write logs`, + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy') + ], + }); + } } diff --git a/lib/schema/configSchema.ts b/lib/schema/configSchema.ts index c499c1053..9a698eac8 100644 --- a/lib/schema/configSchema.ts +++ b/lib/schema/configSchema.ts @@ -781,6 +781,8 @@ const RoleConfig = z.object({ ECSRestApiRole: z.string().max(64), ECSRestApiExRole: z.string().max(64), LambdaExecutionRole: z.string().max(64), + ECSMcpWorkbenchApiRole: z.string().max(64), + ECSMcpWorkbenchApiExRole: z.string().max(64), LambdaConfigurationApiExecutionRole: z.string().max(64), ModelApiRole: z.string().max(64), ModelsSfnLambdaRole: z.string().max(64), diff --git a/lib/serve/mcp-workbench/Dockerfile b/lib/serve/mcp-workbench/Dockerfile index cc72b2705..8879beb75 100644 --- a/lib/serve/mcp-workbench/Dockerfile +++ b/lib/serve/mcp-workbench/Dockerfile @@ -1,4 +1,5 @@ -FROM python:3.13.7-slim +ARG BASE_IMAGE=python:3.13.7-slim +FROM ${BASE_IMAGE} ARG RCLONE_VERSION=v1.71.0 ARG RCLONE_ARCH=amd64 diff --git a/test/cdk/mocks/roles.yaml b/test/cdk/mocks/roles.yaml index 8997e7113..40b7d2ff3 100644 --- a/test/cdk/mocks/roles.yaml +++ b/test/cdk/mocks/roles.yaml @@ -4,9 +4,15 @@ dev: roles: #RagRole / LisaServeIam Roles RagLambdaExecutionRole: TestRagRole + VectorStoreCreatorRole: TestVectorStoreCreatorRole + + #RestAPI ECS Roles ECSRestApiRole: TestEcsRestApiRole ECSRestApiExRole: TestEcsRestApiRole - VectorStoreCreatorRole: TestVectorStoreCreatorRole + + #McpWorkbench ECS Roles + ECSMcpWorkbenchApiExRole: TestEcsMcpWorkbenchApiExRole + ECSMcpWorkbenchApiRole: TestEcsMcpWorkbenchApiRole #ImageBuilderRoles DockerImageBuilderEC2Role: TestImageBuilderRole #ImageBuilder diff --git a/test/cdk/stacks/roleOverrides.test.ts b/test/cdk/stacks/roleOverrides.test.ts index bd6409d2c..47b5f6824 100644 --- a/test/cdk/stacks/roleOverrides.test.ts +++ b/test/cdk/stacks/roleOverrides.test.ts @@ -33,13 +33,13 @@ const stackRolesOverrides: Record = { const stackRoles: Record = { 'LisaApiBase': 5, - 'LisaServe': 7, + 'LisaServe': 5, 'LisaUI': 3, 'LisaNetworking': 0, 'LisaChat': 6, 'LisaCore': 1, 'LisaApiDeployment': 0, - 'LisaIAM': 2, + 'LisaIAM': 5, 'LisaDocs': 4, 'LisaModels': 10, 'LisaRAG': 4, From b686bb65b2c3b8de738db6687d3da7e444ea472f Mon Sep 17 00:00:00 2001 From: bedanley Date: Sun, 26 Oct 2025 21:40:52 -0600 Subject: [PATCH 07/21] Move MCP to separate stack (#525) * Move MCP to separate stack --- Makefile | 14 +- bin/build-images | 22 +- lib/api-base/ecsCluster.ts | 14 + lib/api-base/fastApiContainer.ts | 137 ++------- lib/chat/api/configuration.ts | 89 +++--- lib/chat/chatConstruct.ts | 19 +- lib/core/apiBaseConstruct.ts | 8 - lib/core/apiDeploymentConstruct.ts | 2 +- lib/core/iam/ecs.json | 13 + lib/schema/configSchema.ts | 1 + lib/serve/index.ts | 6 + lib/serve/mcpWorkbenchStack.ts | 280 +++++++++++++++++++ lib/serve/serveApplicationConstruct.ts | 7 + lib/stages.ts | 20 ++ lib/user-interface/userInterfaceConstruct.ts | 4 +- test/cdk/mocks/MockApp.ts | 14 + test/cdk/stacks/roleOverrides.test.ts | 10 +- test/python/integration-setup-test.py | 28 +- test/python/integration-setup-test.sh | 61 +++- 19 files changed, 543 insertions(+), 206 deletions(-) create mode 100644 lib/serve/mcpWorkbenchStack.ts diff --git a/Makefile b/Makefile index 32cb27f8f..f61a501bd 100644 --- a/Makefile +++ b/Makefile @@ -120,6 +120,18 @@ endif # MODEL_BUCKET - S3 bucket containing model artifacts MODEL_BUCKET := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq '.s3BucketModels') +# BASE_URL - Base URL for web UI assets based on domain name and deployment stage +DOMAIN_NAME := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq '.apiGatewayConfig.domainName') +ifeq ($(DOMAIN_NAME), null) +DOMAIN_NAME := $(shell cat $(PROJECT_DIR)/config-base.yaml | yq '.apiGatewayConfig.domainName') +endif + +ifeq ($(DOMAIN_NAME), null) +BASE_URL := /$(DEPLOYMENT_STAGE)/ +else +BASE_URL := / +endif + ################################################################################# # COMMANDS # @@ -270,7 +282,7 @@ listStacks: @npx cdk list buildNpmModules: - npm run build + BASE_URL=$(BASE_URL) npm run build buildArchive: BUILD_ASSETS=true npm run build diff --git a/bin/build-images b/bin/build-images index 52997729f..d5e1725f1 100755 --- a/bin/build-images +++ b/bin/build-images @@ -89,6 +89,13 @@ ecr_login() { fi } +# Function to check if a config parameter is enabled (defaults to true if not present) +should_build_image() { + local param="$1" + local value=$(yq ".${param}" "$ROOT/custom-config.yaml" 2>/dev/null) + [[ "$value" != "false" ]] +} + # Main function to build all images build_all_images() { echo "Starting Docker image builds..." @@ -117,11 +124,16 @@ build_all_images() { rsync -av --exclude='__pycache__' ./lambda/ "$BUILD_DIR/" build_image "Dockerfile" "lisa-batch-ingestion" "$LISA_VERSION" "$RAG_DIR" "NODE_ENV=production" - # lisa-mcp-workbench - MCP_DIR="./lib/serve/mcp-workbench" - build_image "Dockerfile" "lisa-mcp-workbench" "$LISA_VERSION" "$MCP_DIR" \ - "NODE_ENV=production" \ - "BASE_IMAGE=python:3.13.7-slim" + # lisa-mcp-workbench (conditional) + if should_build_image "deployMcpWorkbench"; then + MCP_DIR="./lib/serve/mcp-workbench" + build_image "Dockerfile" "lisa-mcp-workbench" "$LISA_VERSION" "$MCP_DIR" \ + "NODE_ENV=production" \ + "BASE_IMAGE=python:3.13.7-slim" + else + echo "deployMcpWorkbench is disabled, skipping lisa-mcp-workbench build" + echo "" + fi # lisa-tei build_image "Dockerfile" "lisa-tei" "latest" "./lib/serve/ecs-model/embedding/tei" \ diff --git a/lib/api-base/ecsCluster.ts b/lib/api-base/ecsCluster.ts index 73a10908c..193274a08 100644 --- a/lib/api-base/ecsCluster.ts +++ b/lib/api-base/ecsCluster.ts @@ -90,6 +90,15 @@ export class ECSCluster extends Construct { /** Map of all services by identifier */ public readonly services: Partial> = {}; + /** Application Load Balancer */ + public readonly loadBalancer: ApplicationLoadBalancer; + + /** Application Listener */ + public readonly listener: any; + + /** ECS Cluster */ + public readonly cluster: Cluster; + private readonly targetGroups: Partial> = {}; /** @@ -372,6 +381,11 @@ export class ECSCluster extends Construct { createCdkId([identifier, 'ApplicationListener']), listenerProps, ); + + // Expose load balancer, listener, and cluster for shared use + this.loadBalancer = loadBalancer; + this.listener = listener; + this.cluster = cluster; const protocol = listenerProps.port === 443 ? 'https' : 'http'; const domain = diff --git a/lib/api-base/fastApiContainer.ts b/lib/api-base/fastApiContainer.ts index f4cf4e70a..c5b9e6543 100644 --- a/lib/api-base/fastApiContainer.ts +++ b/lib/api-base/fastApiContainer.ts @@ -14,7 +14,7 @@ limitations under the License. */ -import { CfnOutput, Duration } from 'aws-cdk-lib'; +import { CfnOutput } from 'aws-cdk-lib'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { AmiHardwareType, ContainerDefinition } from 'aws-cdk-lib/aws-ecs'; @@ -25,18 +25,10 @@ import { dump as yamlDump } from 'js-yaml'; import { ECSCluster, ECSTasks } from './ecsCluster'; import { BaseProps, Ec2Metadata, ECSConfig, EcsSourceType } from '../schema'; import { Vpc } from '../networking/vpc'; -import { MCP_WORKBENCH_PATH, REST_API_PATH } from '../util'; +import { REST_API_PATH } from '../util'; import * as child_process from 'child_process'; import * as path from 'path'; -import { letIfDefined } from '../util/common-functions'; -import { Bucket } from 'aws-cdk-lib/aws-s3'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; -import * as events from 'aws-cdk-lib/aws-events'; -import * as targets from 'aws-cdk-lib/aws-events-targets'; -import * as lambda from 'aws-cdk-lib/aws-lambda'; -import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; -import { LAMBDA_PATH } from '../util'; -import { getDefaultRuntime } from './utils'; // This is the amount of memory to buffer (or subtract off) from the total instance memory, if we don't include this, // the container can have a hard time finding available RAM resources to start and the tasks will fail deployment @@ -44,6 +36,7 @@ const INSTANCE_MEMORY_RESERVATION = 1024; const SERVE_CONTAINER_MEMORY_RESERVATION = 1024 * 2; const WORKBENCH_CONTAINER_MEMORY_RESERVATION = 1024; + /** * Properties for FastApiContainer Construct. * @@ -71,6 +64,15 @@ export class FastApiContainer extends Construct { /** FastAPI URL **/ public readonly endpoint: string; + /** ECS Cluster **/ + public readonly ecsCluster: any; + + /** Application Load Balancer **/ + public readonly loadBalancer: any; + + /** Application Listener **/ + public readonly listener: any; + /** * @param {Construct} scope - The parent or owner of the construct. * @param {string} id - The unique identifier for the construct within its scope. @@ -125,11 +127,7 @@ export class FastApiContainer extends Construct { path: REST_API_PATH, type: EcsSourceType.ASSET }; - const mcpWorkbenchImage = config.mcpWorkbenchConfig || { - baseImage: config.baseImage, - path: MCP_WORKBENCH_PATH, - type: EcsSourceType.ASSET - }; + const instanceType = 'm5.large'; const healthCheckConfig = { command: ['CMD-SHELL', 'exit 0'], @@ -166,30 +164,10 @@ export class FastApiContainer extends Construct { // set a softlimit of what we expect to use containerMemoryReservationMiB: SERVE_CONTAINER_MEMORY_RESERVATION }, - [ECSTasks.MCPWORKBENCH]: { - environment: {...baseEnvironment, - RCLONE_CONFIG_S3_REGION: config.region, - MCPWORKBENCH_BUCKET: [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(), - }, - containerConfig: { - image: mcpWorkbenchImage, - healthCheckConfig, - environment: {}, - sharedMemorySize: 0, - privileged: true - }, - applicationTarget: { - port: 8000, - priority: 80, - conditions: [ - { type: 'pathPatterns', values: ['/v2/mcp/*'] } - ] - }, - containerMemoryReservationMiB: WORKBENCH_CONTAINER_MEMORY_RESERVATION, - } + }, // reserve at least enough memory for each task and a buffer for the instance to use - containerMemoryBuffer: Ec2Metadata.get(instanceType).memory - (INSTANCE_MEMORY_RESERVATION + SERVE_CONTAINER_MEMORY_RESERVATION + WORKBENCH_CONTAINER_MEMORY_RESERVATION), + containerMemoryBuffer: Ec2Metadata.get(instanceType).memory - (INSTANCE_MEMORY_RESERVATION + SERVE_CONTAINER_MEMORY_RESERVATION + (config.deployMcpWorkbench ? WORKBENCH_CONTAINER_MEMORY_RESERVATION : 0)), instanceType, internetFacing: config.restApiConfig.internetFacing, loadBalancerConfig: { @@ -213,83 +191,7 @@ export class FastApiContainer extends Construct { vpc }); - const workbenchService = apiCluster.services.MCPWORKBENCH; - - // Create Lambda function to handle S3 events and trigger MCP Workbench service redeployment - const s3EventHandlerRole = new Role(this, 'S3EventHandlerRole', { - assumedBy: new ServicePrincipal('lambda.amazonaws.com'), - inlinePolicies: { - 'S3EventHandlerPolicy': new PolicyDocument({ - statements: [ - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'logs:CreateLogGroup', - 'logs:CreateLogStream', - 'logs:PutLogEvents' - ], - resources: [`arn:${config.partition}:logs:*:*:*`] - }), - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'ecs:UpdateService', - 'ecs:DescribeServices', - 'ecs:DescribeClusters' - ], - resources: [ - `arn:${config.partition}:ecs:${config.region}:*:cluster/${workbenchService?.cluster?.clusterName}*`, - `arn:${config.partition}:ecs:${config.region}:*:service/${workbenchService?.cluster?.clusterName}*/${workbenchService?.serviceName}*` - ] - }), - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'ssm:GetParameter' - ], - resources: [ - `arn:${config.partition}:ssm:${config.region}:*:parameter${config.deploymentPrefix}/deploymentName` - ] - }) - ] - }) - } - }); - - const s3EventHandlerLambda = new lambda.Function(this, 'S3EventHandlerLambda', { - runtime: getDefaultRuntime(), - handler: 'mcp_workbench.s3_event_handler.handler', - code: lambda.Code.fromAsset(config.lambdaPath ?? LAMBDA_PATH), - timeout: Duration.minutes(2), - role: s3EventHandlerRole, - environment: { - DEPLOYMENT_PREFIX: config.deploymentPrefix!, - API_NAME: props.apiName, - ECS_CLUSTER_NAME: workbenchService!.cluster?.clusterName, - MCPWORKBENCH_SERVICE_NAME: workbenchService!.serviceName - } - }); - - // Create EventBridge rule to trigger Lambda when S3 objects are created/deleted - const rescanMcpWorkbenchRule = new events.Rule(this, 'RescanMCPWorkbenchRule', { - eventPattern: { - source: ['aws.s3', 'debug'], - detailType: [ - 'Object Created', - 'Object Deleted' - ], - detail: { - bucket: { - name: [[config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase()] - } - } - }, - }); - rescanMcpWorkbenchRule.addTarget(new targets.LambdaFunction(s3EventHandlerLambda, { - retryAttempts: 2, - maxEventAge: Duration.minutes(5) - })); if (tokenTable) { Object.entries(apiCluster.taskRoles).forEach(([, role]) => { @@ -297,11 +199,7 @@ export class FastApiContainer extends Construct { }); } - letIfDefined(apiCluster.taskRoles.MCPWORKBENCH, (taskRole) => { - const bucketName = [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(); - const workbenchBucket = Bucket.fromBucketName(scope, 'MCPWorkbenchBucket', bucketName); - workbenchBucket.grantRead(taskRole); - }); + this.endpoint = apiCluster.endpointUrl; @@ -313,6 +211,9 @@ export class FastApiContainer extends Construct { // Update this.containers = Object.values(apiCluster.containers); this.taskRoles = apiCluster.taskRoles; + this.ecsCluster = apiCluster.cluster; + this.loadBalancer = apiCluster.loadBalancer; + this.listener = apiCluster.listener; // CFN output new CfnOutput(this, `${props.apiName}Url`, { diff --git a/lib/chat/api/configuration.ts b/lib/chat/api/configuration.ts index 7531bf708..26824f674 100644 --- a/lib/chat/api/configuration.ts +++ b/lib/chat/api/configuration.ts @@ -46,7 +46,7 @@ type ConfigurationApiProps = { rootResourceId: string; securityGroups: ISecurityGroup[]; vpc: Vpc; - mcpApi: McpApi; + mcpApi?: McpApi; } & BaseProps; /** @@ -88,11 +88,8 @@ export class ConfigurationApi extends Construct { removalPolicy: config.removalPolicy, }); - const mcpServersTable = dynamodb.Table.fromTableName(this, 'McpServersTable', mcpApi.mcpServersTableNameParameter.stringValue); const lambdaRole: IRole = createLambdaRole(this, config.deploymentName, 'ConfigurationApi', this.configTable.tableArn, config.roles?.LambdaConfigurationApiExecutionRole); - mcpServersTable.grantReadWriteData(lambdaRole); - // Populate the App Config table with default config const date = new Date(); new AwsCustomResource(this, 'lisa-init-ddb-config', { @@ -103,36 +100,42 @@ export class ConfigurationApi extends Construct { parameters: { TableName: this.configTable.tableName, Item: { - 'versionId': {'N': '0'}, - 'changedBy': {'S': 'System'}, - 'configScope': {'S': 'global'}, - 'changeReason': {'S': 'Initial deployment default config'}, - 'createdAt': {'S': Math.round(date.getTime() / 1000).toString()}, - 'configuration': {'M': { - 'enabledComponents': {'M': { - 'deleteSessionHistory': {'BOOL': 'True'}, - 'viewMetaData': {'BOOL': 'True'}, - 'editKwargs': {'BOOL': 'True'}, - 'editPromptTemplate': {'BOOL': 'True'}, - 'editChatHistoryBuffer': {'BOOL': 'True'}, - 'editNumOfRagDocument': {'BOOL': 'True'}, - 'uploadRagDocs': {'BOOL': 'True'}, - 'uploadContextDocs': {'BOOL': 'True'}, - 'documentSummarization': {'BOOL': 'True'}, - 'showRagLibrary': {'BOOL': 'True'}, - 'showMcpWorkbench': {'BOOL': 'False'}, - 'showPromptTemplateLibrary': {'BOOL': 'True'}, - 'mcpConnections': {'BOOL': 'True'}, - 'modelLibrary': {'BOOL': 'True'}, - 'encryptSession': {'BOOL': 'False'}, - }}, - 'systemBanner': {'M': { - 'isEnabled': {'BOOL': 'False'}, - 'text': {'S': ''}, - 'textColor': {'S': ''}, - 'backgroundColor': {'S': ''} - }} - }} + 'versionId': { 'N': '0' }, + 'changedBy': { 'S': 'System' }, + 'configScope': { 'S': 'global' }, + 'changeReason': { 'S': 'Initial deployment default config' }, + 'createdAt': { 'S': Math.round(date.getTime() / 1000).toString() }, + 'configuration': { + 'M': { + 'enabledComponents': { + 'M': { + 'deleteSessionHistory': { 'BOOL': 'True' }, + 'viewMetaData': { 'BOOL': 'True' }, + 'editKwargs': { 'BOOL': 'True' }, + 'editPromptTemplate': { 'BOOL': 'True' }, + 'editChatHistoryBuffer': { 'BOOL': 'True' }, + 'editNumOfRagDocument': { 'BOOL': 'True' }, + 'uploadRagDocs': { 'BOOL': 'True' }, + 'uploadContextDocs': { 'BOOL': 'True' }, + 'documentSummarization': { 'BOOL': 'True' }, + 'showRagLibrary': { 'BOOL': 'True' }, + 'showMcpWorkbench': { 'BOOL': 'False' }, + 'showPromptTemplateLibrary': { 'BOOL': 'True' }, + 'mcpConnections': { 'BOOL': 'True' }, + 'modelLibrary': { 'BOOL': 'True' }, + 'encryptSession': { 'BOOL': 'False' }, + } + }, + 'systemBanner': { + 'M': { + 'isEnabled': { 'BOOL': 'False' }, + 'text': { 'S': '' }, + 'textColor': { 'S': '' }, + 'backgroundColor': { 'S': '' } + } + } + } + } }, }, }, @@ -146,13 +149,15 @@ export class ConfigurationApi extends Construct { const fastApiEndpoint = StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/serve/endpoint`); - const environment = { + let environment = { CONFIG_TABLE_NAME: this.configTable.tableName, - FASTAPI_ENDPOINT: fastApiEndpoint, - // add MCP_SERVERS_TABLE_NAME so we can update it if the configuration changes - MCP_SERVERS_TABLE_NAME: mcpServersTable.tableName + FASTAPI_ENDPOINT: fastApiEndpoint }; + if (mcpApi) { + this.createMcpApiTable(mcpApi, lambdaRole, environment); + } + // Create API Lambda functions const apis: PythonLambdaFunction[] = [ { @@ -196,4 +201,12 @@ export class ConfigurationApi extends Construct { } }); } + + private createMcpApiTable (mcpApi: McpApi, lambdaRole: IRole, environment: Record) { + const mcpServersTable = dynamodb.Table.fromTableName(this, 'McpServersTable', mcpApi.mcpServersTableNameParameter.stringValue); + mcpServersTable.grantReadWriteData(lambdaRole); + + // add MCP_SERVERS_TABLE_NAME so we can update it if the configuration changes + environment.MCP_SERVERS_TABLE_NAME = mcpServersTable.tableName; + } } diff --git a/lib/chat/chatConstruct.ts b/lib/chat/chatConstruct.ts index 470f98f7e..c3b5355c3 100644 --- a/lib/chat/chatConstruct.ts +++ b/lib/chat/chatConstruct.ts @@ -51,14 +51,15 @@ export class LisaChatApplicationConstruct extends Construct { const { authorizer, config, restApiId, rootResourceId, securityGroups, vpc } = props; - const mcpApi = new McpApi(scope, 'McpApi', { - authorizer, - config, - restApiId, - rootResourceId, - securityGroups, - vpc, - }); + const mcpApi = config.deployMcpWorkbench ? + new McpApi(scope, 'McpApi', { + authorizer, + config, + restApiId, + rootResourceId, + securityGroups, + vpc, + }) : undefined; // Create Configuration API first to get the configuration table const configurationApi = new ConfigurationApi(scope, 'ConfigurationApi', { @@ -68,7 +69,7 @@ export class LisaChatApplicationConstruct extends Construct { rootResourceId, securityGroups, vpc, - mcpApi + ...(config.deployMcpWorkbench ? { mcpApi } : {}) }); // Add REST API Lambdas to APIGW diff --git a/lib/core/apiBaseConstruct.ts b/lib/core/apiBaseConstruct.ts index 7cb27dcc4..8467309c7 100644 --- a/lib/core/apiBaseConstruct.ts +++ b/lib/core/apiBaseConstruct.ts @@ -23,7 +23,6 @@ import { BaseProps } from '../schema'; import { Vpc } from '../networking/vpc'; import { Role } from 'aws-cdk-lib/aws-iam'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; -import McpWorkbenchConstruct from '../serve/mcpWorkbenchConstruct'; export type LisaApiBaseProps = { vpc: Vpc; @@ -80,13 +79,6 @@ export class LisaApiBaseConstruct extends Construct { binaryMediaTypes: ['font/*', 'image/*'], }); - new McpWorkbenchConstruct(this, id + 'McpWorkbench', { - ...props, - authorizer: this.authorizer!, - restApiId: restApi.restApiId, - rootResourceId: restApi.restApiRootResourceId, - securityGroups: [props.vpc.securityGroups.ecsModelAlbSg], - }); this.restApi = restApi; this.restApiId = restApi.restApiId; diff --git a/lib/core/apiDeploymentConstruct.ts b/lib/core/apiDeploymentConstruct.ts index 288bd486a..a02f9b11b 100644 --- a/lib/core/apiDeploymentConstruct.ts +++ b/lib/core/apiDeploymentConstruct.ts @@ -49,7 +49,7 @@ export class LisaApiDeploymentConstruct extends Construct { const api_url = `https://${restApiId}.execute-api.${Aws.REGION}.${Aws.URL_SUFFIX}/${config.deploymentStage}`; new StringParameter(scope, 'LisaApiDeploymentStringParameter', { - parameterName: `${config.deploymentPrefix}/${config.deploymentName}/${config.appName}/LisaApiUrl`, + parameterName: `${config.deploymentPrefix}/LisaApiUrl`, stringValue: api_url, description: 'API Gateway URL for LISA', }); diff --git a/lib/core/iam/ecs.json b/lib/core/iam/ecs.json index 8f35ae796..8d254c369 100644 --- a/lib/core/iam/ecs.json +++ b/lib/core/iam/ecs.json @@ -185,6 +185,19 @@ "Effect": "Allow", "Action": "secretsmanager:GetSecretValue", "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:${AWS::Partition}:s3:::*-mcpworkbench-*", + "arn:${AWS::Partition}:s3:::*-mcpworkbench-*/*" + ] } ] } diff --git a/lib/schema/configSchema.ts b/lib/schema/configSchema.ts index 9a698eac8..573c5f094 100644 --- a/lib/schema/configSchema.ts +++ b/lib/schema/configSchema.ts @@ -847,6 +847,7 @@ export const RawConfigObject = z.object({ deployDocs: z.boolean().default(true).describe('Whether to deploy docs stacks.'), deployUi: z.boolean().default(true).describe('Whether to deploy UI stacks.'), deployMetrics: z.boolean().default(true).describe('Whether to deploy Metrics stack.'), + deployMcpWorkbench: z.boolean().default(true).describe('Whether to deploy MCP Workbench stack.'), logLevel: z.union([z.literal('DEBUG'), z.literal('INFO'), z.literal('WARNING'), z.literal('ERROR')]) .default('DEBUG') .describe('Log level for application.'), diff --git a/lib/serve/index.ts b/lib/serve/index.ts index 4124f5a9e..b78de21e4 100644 --- a/lib/serve/index.ts +++ b/lib/serve/index.ts @@ -32,6 +32,9 @@ export class LisaServeApplicationStack extends Stack { public readonly modelsPs: StringParameter; public readonly endpointUrl: StringParameter; public readonly tokenTable?: ITable; + public readonly ecsCluster: any; + public readonly loadBalancer: any; + public readonly listener: any; /** * @param {Construct} scope - The parent or owner of the construct. @@ -47,5 +50,8 @@ export class LisaServeApplicationStack extends Stack { this.modelsPs = app.modelsPs; this.restApi = app.restApi; this.tokenTable = app.tokenTable; + this.ecsCluster = app.ecsCluster; + this.loadBalancer = app.loadBalancer; + this.listener = app.listener; } } diff --git a/lib/serve/mcpWorkbenchStack.ts b/lib/serve/mcpWorkbenchStack.ts new file mode 100644 index 000000000..df6386df4 --- /dev/null +++ b/lib/serve/mcpWorkbenchStack.ts @@ -0,0 +1,280 @@ +/** + 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 { Duration, Stack, StackProps } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { BaseProps, EcsSourceType } from '../schema'; +import McpWorkbenchConstruct from './mcpWorkbenchConstruct'; +import { Vpc } from '../networking/vpc'; +import { ICluster, Ec2Service, Ec2TaskDefinition, Protocol, LogDriver, HealthCheck } from 'aws-cdk-lib/aws-ecs'; +import { MCP_WORKBENCH_PATH } from '../util'; +import { dump as yamlDump } from 'js-yaml'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { LAMBDA_PATH } from '../util'; +import { getDefaultRuntime } from '../api-base/utils'; +import { createCdkId } from '../core/utils'; +import { CodeFactory } from '../util'; +import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; + +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { Port } from 'aws-cdk-lib/aws-ec2'; +import { ListenerCondition } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; + +export type McpWorkbenchStackProps = { + vpc: Vpc; + restApiId: string; + rootResourceId: string; + authorizerId: string; + ecsCluster: ICluster; + loadBalancer: any; + listener: any; +} & BaseProps & StackProps; + +export class McpWorkbenchStack extends Stack { + constructor (scope: Construct, id: string, props: McpWorkbenchStackProps) { + super(scope, id, props); + + const { config, vpc, restApiId, rootResourceId, authorizerId, ecsCluster, loadBalancer, listener } = props; + + // Import authorizer + const authorizer = { authorizerId }; + + new McpWorkbenchConstruct(this, 'McpWorkbench', { + ...props, + authorizer: authorizer as any, + restApiId, + rootResourceId, + securityGroups: [vpc.securityGroups.ecsModelAlbSg], + }); + + // Create MCP Workbench ECS Service on shared cluster + this.createMcpWorkbenchEcsService(config, vpc, ecsCluster, loadBalancer, listener); + } + + private createMcpWorkbenchEcsService (config: any, vpc: Vpc, ecsCluster: ICluster, loadBalancer: any, listener: any) { + const baseEnvironment: Record = { + LOG_LEVEL: config.logLevel, + AWS_REGION: config.region, + AWS_REGION_NAME: config.region, + LITELLM_KEY: config.litellmConfig.db_key, + OPENAI_API_KEY: config.litellmConfig.db_key, + USE_AUTH: 'true', + AUTHORITY: config.authConfig!.authority, + CLIENT_ID: config.authConfig!.clientId, + ADMIN_GROUP: config.authConfig!.adminGroup, + USER_GROUP: config.authConfig!.userGroup, + JWT_GROUPS_PROP: config.authConfig!.jwtGroupsProperty, + RCLONE_CONFIG_S3_REGION: config.region, + MCPWORKBENCH_BUCKET: [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(), + }; + + const mcpWorkbenchImage = config.mcpWorkbenchConfig || { + baseImage: config.baseImage, + path: MCP_WORKBENCH_PATH, + type: EcsSourceType.ASSET + }; + + const buildArgs: Record = { + BASE_IMAGE: config.baseImage, + PYPI_INDEX_URL: config.pypiConfig.indexUrl, + PYPI_TRUSTED_HOST: config.pypiConfig.trustedHost, + LITELLM_CONFIG: yamlDump(config.litellmConfig), + }; + + // Create CloudWatch log group + const logGroup = new LogGroup(this, createCdkId([config.deploymentPrefix, 'MCPWorkbench', 'LogGroup']), { + logGroupName: `/aws/ecs/${config.deploymentName}-${config.deploymentStage}-MCPWorkbench`, + retention: RetentionDays.ONE_WEEK, + removalPolicy: config.removalPolicy + }); + + // Get task role from parameter store + const taskRole = Role.fromRoleArn( + this, + 'MCPWorkbenchTaskRole', + StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/MCPWORKBENCH`), + ); + const executionRole = Role.fromRoleArn( + this, + 'MCPWorkbenchExecutionRole', + StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/MCPWORKBENCHEX`), + ); + + // Create task definition + const taskDefinition = new Ec2TaskDefinition(this, createCdkId(['MCPWorkbench', 'Ec2TaskDefinition']), { + family: createCdkId([config.deploymentName, 'MCPWorkbench'], 32, 2), + taskRole, + executionRole, + }); + + // Grant CloudWatch logs write permissions + logGroup.grantWrite(taskRole); + logGroup.grantWrite(executionRole); + + const healthCheckConfig = { + command: ['CMD-SHELL', 'exit 0'], + interval: 10, + startPeriod: 30, + timeout: 5, + retries: 3 + }; + + const containerHealthCheck: HealthCheck = { + command: healthCheckConfig.command, + interval: Duration.seconds(healthCheckConfig.interval), + startPeriod: Duration.seconds(healthCheckConfig.startPeriod), + timeout: Duration.seconds(healthCheckConfig.timeout), + retries: healthCheckConfig.retries, + }; + + const image = CodeFactory.createImage(mcpWorkbenchImage, this, 'MCPWorkbench', buildArgs); + + taskDefinition.addContainer(createCdkId(['MCPWorkbench', 'Container']), { + containerName: createCdkId([config.deploymentName, 'MCPWorkbench'], 32, 2), + image, + environment: baseEnvironment, + logging: LogDriver.awsLogs({ + logGroup: logGroup, + streamPrefix: 'MCPWorkbench' + }), + memoryReservationMiB: 1024, + portMappings: [{ hostPort: 0, containerPort: 8000, protocol: Protocol.TCP }], + healthCheck: containerHealthCheck, + privileged: true, + }); + + // Create ECS service + const service = new Ec2Service(this, createCdkId([config.deploymentName, 'MCPWorkbench', 'Ec2Svc']), { + cluster: ecsCluster, + serviceName: createCdkId(['MCPWorkbench'], 32, 2), + taskDefinition: taskDefinition, + circuitBreaker: !config.region.includes('iso') ? { rollback: true } : undefined, + }); + + const scalableTaskCount = service.autoScaleTaskCount({ + minCapacity: 1, + maxCapacity: 10 + }); + + // Connect service to shared load balancer + service.connections.allowFrom(loadBalancer, Port.allTcp()); + + // Create target group for MCP Workbench + const targetGroup = listener.addTargets(createCdkId(['REST', 'MCPWorkbench', 'TgtGrp']), { + targetGroupName: createCdkId([config.deploymentName, 'REST', 'MCPWorkbench'], 32, 2).toLowerCase(), + healthCheck: { + path: '/health', + interval: Duration.seconds(60), + timeout: Duration.seconds(30), + healthyThresholdCount: 2, + unhealthyThresholdCount: 10, + }, + port: 80, + targets: [service], + priority: 80, + conditions: [ListenerCondition.pathPatterns(['/v2/mcp/*'])] + }); + + scalableTaskCount.scaleOnRequestCount(createCdkId(['REST', 'MCPWorkbench', 'ScalingPolicy']), { + requestsPerTarget: 500, + targetGroup, + scaleInCooldown: Duration.seconds(60), + scaleOutCooldown: Duration.seconds(60) + }); + + // Create S3 event handler for MCP Workbench + this.createS3EventHandler(config, service); + } + + private createS3EventHandler (config: any, workbenchService: Ec2Service) { + const s3EventHandlerRole = new Role(this, 'S3EventHandlerRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), + inlinePolicies: { + 'S3EventHandlerPolicy': new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents' + ], + resources: [`arn:${config.partition}:logs:*:*:*`] + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'ecs:UpdateService', + 'ecs:DescribeServices', + 'ecs:DescribeClusters' + ], + resources: [ + `arn:${config.partition}:ecs:${config.region}:*:cluster/${workbenchService.cluster.clusterName}*`, + `arn:${config.partition}:ecs:${config.region}:*:service/${workbenchService.cluster.clusterName}*/${workbenchService.serviceName}*` + ] + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'ssm:GetParameter' + ], + resources: [ + `arn:${config.partition}:ssm:${config.region}:*:parameter${config.deploymentPrefix}/deploymentName` + ] + }) + ] + }) + } + }); + + const s3EventHandlerLambda = new lambda.Function(this, 'S3EventHandlerLambda', { + runtime: getDefaultRuntime(), + handler: 'mcp_workbench.s3_event_handler.handler', + code: lambda.Code.fromAsset(config.lambdaPath ?? LAMBDA_PATH), + timeout: Duration.minutes(2), + role: s3EventHandlerRole, + environment: { + DEPLOYMENT_PREFIX: config.deploymentPrefix!, + API_NAME: 'MCPWorkbench', + ECS_CLUSTER_NAME: workbenchService.cluster.clusterName, + MCPWORKBENCH_SERVICE_NAME: workbenchService.serviceName + } + }); + + const rescanMcpWorkbenchRule = new events.Rule(this, 'RescanMCPWorkbenchRule', { + eventPattern: { + source: ['aws.s3', 'debug'], + detailType: [ + 'Object Created', + 'Object Deleted' + ], + detail: { + bucket: { + name: [[config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase()] + } + } + }, + }); + + rescanMcpWorkbenchRule.addTarget(new targets.LambdaFunction(s3EventHandlerLambda, { + retryAttempts: 2, + maxEventAge: Duration.minutes(5) + })); + } +} diff --git a/lib/serve/serveApplicationConstruct.ts b/lib/serve/serveApplicationConstruct.ts index 26cfa9fbf..d7179b111 100644 --- a/lib/serve/serveApplicationConstruct.ts +++ b/lib/serve/serveApplicationConstruct.ts @@ -19,6 +19,7 @@ import { Credentials, DatabaseInstance, DatabaseInstanceEngine } from 'aws-cdk-l import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { Code, Function, IFunction, ILayerVersion, LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { ICluster } from 'aws-cdk-lib/aws-ecs'; import { FastApiContainer } from '../api-base/fastApiContainer'; import { createCdkId } from '../core/utils'; import { Vpc } from '../networking/vpc'; @@ -57,6 +58,9 @@ export class LisaServeApplicationConstruct extends Construct { public readonly modelsPs: StringParameter; public readonly endpointUrl: StringParameter; public readonly tokenTable?: ITable; + public readonly ecsCluster: ICluster; + public readonly loadBalancer: any; + public readonly listener: any; /** * @param {Stack} scope - The parent or owner of the construct. @@ -324,6 +328,9 @@ export class LisaServeApplicationConstruct extends Construct { // Update this.restApi = restApi; + this.ecsCluster = restApi.ecsCluster; + this.loadBalancer = restApi.loadBalancer; + this.listener = restApi.listener; // Grant permissions after restApi is fully constructed // Additional permissions for REST API Role diff --git a/lib/stages.ts b/lib/stages.ts index 739810fc0..45221370e 100644 --- a/lib/stages.ts +++ b/lib/stages.ts @@ -43,6 +43,7 @@ import { LisaNetworkingStack } from './networking'; import { LisaRagStack } from './rag'; import { BaseProps, stackSynthesizerType } from './schema'; import { LisaServeApplicationStack } from './serve'; +import { McpWorkbenchStack } from './serve/mcpWorkbenchStack'; import { UserInterfaceStack } from './user-interface'; import { LisaDocsStack } from './docs'; import { LisaMetricsStack } from './metrics'; @@ -271,6 +272,25 @@ export class LisaServeApplicationStage extends Stage { apiDeploymentStack.addDependency(modelsApiDeploymentStack); this.stacks.push(modelsApiDeploymentStack); + if (config.deployMcpWorkbench) { + const mcpWorkbenchStack = new McpWorkbenchStack(this, 'LisaMcpWorkbench', { + ...baseStackProps, + stackName: createCdkId([config.deploymentName, config.appName, 'mcp-workbench', config.deploymentStage]), + description: `LISA-mcp-workbench: ${config.deploymentName}-${config.deploymentStage}`, + vpc: networkingStack.vpc, + restApiId: apiBaseStack.restApiId, + rootResourceId: apiBaseStack.rootResourceId, + authorizerId: apiBaseStack.authorizer?.authorizerId || '', + ecsCluster: serveStack.ecsCluster, + loadBalancer: serveStack.loadBalancer, + listener: serveStack.listener, + }); + mcpWorkbenchStack.addDependency(apiBaseStack); + mcpWorkbenchStack.addDependency(serveStack); + apiDeploymentStack.addDependency(mcpWorkbenchStack); + this.stacks.push(mcpWorkbenchStack); + } + if (config.deployRag) { const ragStack = new LisaRagStack(this, 'LisaRAG', { ...baseStackProps, diff --git a/lib/user-interface/userInterfaceConstruct.ts b/lib/user-interface/userInterfaceConstruct.ts index dd74b348f..5b7efea25 100644 --- a/lib/user-interface/userInterfaceConstruct.ts +++ b/lib/user-interface/userInterfaceConstruct.ts @@ -226,7 +226,7 @@ export class UserInterfaceConstruct extends Construct { ].join(' && '), ], environment: { - BASE_URL: `${uriPrefix}` + BASE_URL: uriPrefix }, local: { tryBundle (outputDir: string) { @@ -235,7 +235,7 @@ export class UserInterfaceConstruct extends Construct { stdio: 'inherit', env: { ...process.env, - BASE_URL: `${uriPrefix}` + BASE_URL: uriPrefix }, }; execSync(`npm --silent --prefix "${ROOT_PATH}" ci`, options); diff --git a/test/cdk/mocks/MockApp.ts b/test/cdk/mocks/MockApp.ts index 45e7b63d8..38b96b4b0 100644 --- a/test/cdk/mocks/MockApp.ts +++ b/test/cdk/mocks/MockApp.ts @@ -22,6 +22,7 @@ import { ARCHITECTURE, CoreStack } from '../../../lib/core/index'; import { LisaApiDeploymentStack } from '../../../lib/core/api_deployment'; import { LisaServeIAMStack } from '../../../lib/iam/iam_stack'; import { LisaServeApplicationStack } from '../../../lib/serve/index'; +import { McpWorkbenchStack } from '../../../lib/serve/mcpWorkbenchStack'; import { UserInterfaceStack } from '../../../lib/user-interface/index'; import { LisaMetricsStack } from '../../../lib/metrics/index'; import ConfigParser from './ConfigParser'; @@ -158,6 +159,18 @@ export default class MockApp { vpc: networkingStack.vpc, }); + const mcpWorkbenchStack = new McpWorkbenchStack(app, 'LisaMcpWorkbench', { + ...baseStackProps, + stackName: 'LisaMcpWorkbench', + vpc: networkingStack.vpc, + restApiId: apiBaseStack.restApiId, + rootResourceId: apiBaseStack.rootResourceId, + authorizerId: apiBaseStack.authorizer?.authorizerId || '', + ecsCluster: serveStack.ecsCluster, + loadBalancer: serveStack.loadBalancer, + listener: serveStack.listener, + }); + const stacks = [ networkingStack, iamStack, @@ -171,6 +184,7 @@ export default class MockApp { coreStack, modelsStack, ragStack, + mcpWorkbenchStack ]; return { app, stacks }; diff --git a/test/cdk/stacks/roleOverrides.test.ts b/test/cdk/stacks/roleOverrides.test.ts index 47b5f6824..f85d6d60f 100644 --- a/test/cdk/stacks/roleOverrides.test.ts +++ b/test/cdk/stacks/roleOverrides.test.ts @@ -21,19 +21,20 @@ import { Roles } from '../../../lib/core/iam/roles'; import { Stack } from 'aws-cdk-lib'; const stackRolesOverrides: Record = { - 'LisaApiBase': 4, - 'LisaServe': 5, + 'LisaApiBase': 1, + 'LisaServe': 4, 'LisaUI': 1, 'LisaDocs': 2, 'LisaRAG': 4, 'LisaChat': 1, 'LisaCore': 1, 'LisaModels': 1, + 'LisaMcpWorkbench': 4, }; const stackRoles: Record = { - 'LisaApiBase': 5, - 'LisaServe': 5, + 'LisaApiBase': 2, + 'LisaServe': 4, 'LisaUI': 3, 'LisaNetworking': 0, 'LisaChat': 6, @@ -44,6 +45,7 @@ const stackRoles: Record = { 'LisaModels': 10, 'LisaRAG': 4, 'LisaMetrics': 1, + 'LisaMcpWorkbench': 4 }; describe('Verify role overrides', () => { diff --git a/test/python/integration-setup-test.py b/test/python/integration-setup-test.py index c58a3b470..f8b24318a 100644 --- a/test/python/integration-setup-test.py +++ b/test/python/integration-setup-test.py @@ -40,7 +40,7 @@ RAG_PIPELINE_BUCKET = "lisa-rag-pipeline" -def get_management_key(deployment_name: str) -> str: +def get_management_key(deployment_name: str, deployment_stage: str) -> str: """Retrieve management key from AWS Secrets Manager. Args: @@ -50,6 +50,7 @@ def get_management_key(deployment_name: str) -> str: str: The management API key """ secret_name = f"{deployment_name}-lisa-management-key" + print(f" Looking for secret: {secret_name}") try: secrets_client = boto3.client("secretsmanager") @@ -93,7 +94,7 @@ def create_api_token(deployment_name: str, api_key: str) -> str: raise -def setup_authentication(deployment_name: str) -> Dict[str, str]: +def setup_authentication(deployment_name: str, deployment_stage: str) -> Dict[str, str]: """Set up authentication for LISA API calls. Args: @@ -105,7 +106,7 @@ def setup_authentication(deployment_name: str) -> Dict[str, str]: print(f"🔑 Setting up authentication for deployment: {deployment_name}") # Get management key from AWS Secrets Manager - api_key = get_management_key(deployment_name) + api_key = get_management_key(deployment_name, deployment_stage) # Create API token in DynamoDB (optional - for tracking purposes) try: @@ -190,9 +191,14 @@ def model_exists(lisa_client: LisaApi, model_id: str) -> bool: def repository_exists(lisa_client: LisaApi, repository_id: str) -> bool: """Check if a repository already exists.""" try: - lisa_client.get_repository(repository_id) - return True - except Exception: + repositories = lisa_client.list_repositories() + print(f" DEBUG: list_repositories() returned {len(repositories)} repositories") + for repo in repositories: + if repo.get("repositoryId") == repository_id: + return True + return False + except Exception as e: + print(f" DEBUG: list_repositories() raised exception: {type(e).__name__}: {e}") return False @@ -222,7 +228,7 @@ def create_bedrock_model( "modelDescription": "", "modelType": model_type, "modelUrl": "", - "streaming": True, + "streaming": True if model_type != "embedding" else False, "features": features, "allowedGroups": None, } @@ -551,6 +557,8 @@ def main(): parser.add_argument("--url", required=True, help="LISA ALB URL") parser.add_argument("--api", required=True, help="LISA API URL") parser.add_argument("--deployment-name", required=True, help="LISA deployment name for authentication") + parser.add_argument("--deployment-stage", required=True, help="LISA deployment stage for authentication") + parser.add_argument("--deployment-prefix", required=True, help="LISA deployment prefix") parser.add_argument("--verify", default="false", help="Verify SSL certificates") parser.add_argument("--profile", help="AWS profile to use") parser.add_argument("--cleanup", action="store_true", help="Clean up resources after test") @@ -560,17 +568,19 @@ def main(): # Convert verify to boolean verify_ssl = args.verify.lower() not in ["false", "0", "no", "off"] - print("🚀 LISA Integration Setup Test Starting...") print(f"ALB URL: {args.url}") print(f"API URL: {args.api}") print(f"Deployment Name: {args.deployment_name}") + print(f"Deployment Stage: {args.deployment_stage}") + print(f"Deployment Prefix: {args.deployment_prefix}") print(f"Verify SSL: {verify_ssl}") print(f"AWS Profile: {args.profile}") try: # Setup authentication - auth_headers = setup_authentication(args.deployment_name) + + auth_headers = setup_authentication(args.deployment_name, args.deployment_stage) # Initialize LISA client with authentication lisa_client = LisaApi(url=args.api, verify=verify_ssl, headers=auth_headers) diff --git a/test/python/integration-setup-test.sh b/test/python/integration-setup-test.sh index 73aa11125..40218500f 100755 --- a/test/python/integration-setup-test.sh +++ b/test/python/integration-setup-test.sh @@ -80,32 +80,71 @@ if [ -z $VERIFY ]; then fi echo "Using settings: PROFILE-${PROFILE}, DEPLOYMENT_NAME-${DEPLOYMENT_NAME}, APP_NAME-${APP_NAME}, DEPLOYMENT_STAGE-${DEPLOYMENT_STAGE}, REGION-${REGION}, VERIFY-${VERIFY}, API_URL-${API_URL}, ALB_URL-${ALB_URL}" +PREFIX="/${DEPLOYMENT_STAGE}/${DEPLOYMENT_NAME}/${APP_NAME}" +echo "Prefix: ${PREFIX}" + +# Check for AWS credentials - try with env vars first, then profile +AWS_ARGS="" +if [ -n "${AWS_ACCESS_KEY_ID}" ] && [ -n "${AWS_SECRET_ACCESS_KEY}" ]; then + echo "Using AWS credentials from environment variables" + if ! aws sts get-caller-identity --region "${REGION}" &>/dev/null; then + echo "❌ Error: AWS credentials from environment are invalid" + exit 1 + fi +else + echo "Using AWS profile: ${PROFILE}" + AWS_ARGS="--profile ${PROFILE}" + if ! aws sts get-caller-identity ${AWS_ARGS} --region "${REGION}" &>/dev/null; then + echo "❌ Error: AWS credentials not configured for profile '${PROFILE}'" + exit 1 + fi +fi if [ -z "$ALB_URL" ]; then echo "Grabbing ALB from SSM..." + SSM_PARAM="${PREFIX}/lisaServeRestApiUri" + echo " Checking SSM parameter: ${SSM_PARAM}" + echo " Using profile: ${PROFILE}, region: ${REGION}" ALB_URL=$(aws ssm get-parameter \ - --name "/${DEPLOYMENT_STAGE}/${DEPLOYMENT_NAME}/${APP_NAME}/lisaServeRestApiUri" \ + --name "${SSM_PARAM}" \ + --region "${REGION}" \ + ${AWS_ARGS} \ --query "Parameter.Value" \ - --output text 2>/dev/null || echo "") + --output text 2>&1) + ALB_EXIT_CODE=$? - if [ -z "$ALB_URL" ] || [ "$ALB_URL" = "None" ]; then + if [ $ALB_EXIT_CODE -ne 0 ]; then + echo " ❌ SSM parameter not found or access denied" + ALB_URL="" + elif [ -z "$ALB_URL" ] || [ "$ALB_URL" = "None" ]; then echo "⚠️ Could not retrieve ALB URL from SSM. You may need to provide it manually with --alb-url" ALB_URL="" else - echo "Using ALB: ${ALB_URL}" + echo "✓ Using ALB: ${ALB_URL}" fi fi if [ -z "$API_URL" ]; then - echo "Grabbing API from CFN..." - API_URL=$(aws cloudformation describe-stacks --stack-name ${DEPLOYMENT_NAME}-${APP_NAME}-api-deployment-${DEPLOYMENT_STAGE} --region ${REGION} \ - --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text 2>/dev/null || echo "") + echo "Grabbing API from SSM..." + SSM_PARAM="${PREFIX}/LisaApiUrl" + echo " Checking SSM parameter: ${SSM_PARAM}" + echo " Using profile: ${PROFILE}, region: ${REGION}" + API_URL=$(aws ssm get-parameter \ + --name "${SSM_PARAM}" \ + --region "${REGION}" \ + ${AWS_ARGS} \ + --query "Parameter.Value" \ + --output text 2>&1) + API_EXIT_CODE=$? - if [ -z "$API_URL" ] || [ "$API_URL" = "None" ]; then - echo "⚠️ Could not retrieve API URL from CloudFormation. You may need to provide it manually with --rest-url" + if [ $API_EXIT_CODE -ne 0 ]; then + echo " ❌ SSM parameter not found or access denied" + API_URL="" + elif [ -z "$API_URL" ] || [ "$API_URL" = "None" ]; then + echo "⚠️ Could not retrieve API URL from SSM. You may need to provide it manually with --rest-url" API_URL="" else - echo "Using API: ${API_URL}" + echo "✓ Using API: ${API_URL}" fi fi @@ -127,7 +166,7 @@ if [ -z "$ALB_URL" ] || [ -z "$API_URL" ]; then fi # Construct Python script arguments -PYTHON_ARGS="--url $ALB_URL --api $API_URL --deployment-name $DEPLOYMENT_NAME --verify $VERIFY" +PYTHON_ARGS="--url $ALB_URL --api $API_URL --deployment-name $DEPLOYMENT_NAME --deployment-stage $DEPLOYMENT_STAGE --deployment-prefix $PREFIX --verify $VERIFY" if [ ! -z "$PROFILE" ]; then PYTHON_ARGS="$PYTHON_ARGS --profile $PROFILE" From b8d459d561dbd7e0d242cc274334878bb0086fcc Mon Sep 17 00:00:00 2001 From: Dustin Sweigart Date: Mon, 27 Oct 2025 15:19:38 +0000 Subject: [PATCH 08/21] updated flake --- .gitignore | 1 + flake.lock | 8 ++++---- flake.nix | 6 ++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 0097dc556..5c733858b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.direnv *.js !tailwind.config.js !postcss.config.js diff --git a/flake.lock b/flake.lock index 5cc6d190c..5aec80c83 100644 --- a/flake.lock +++ b/flake.lock @@ -20,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1760284886, - "narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=", + "lastModified": 1761468971, + "narHash": "sha256-vY2OLVg5ZTobdroQKQQSipSIkHlxOTrIF1fsMzPh8w8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43", + "rev": "78e34d1667d32d8a0ffc3eba4591ff256e80576e", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "nixos-25.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 5c018f6a0..a10634c69 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ inputs = { # Use the unstable channel for latest package versions - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; # Utility functions for creating flakes that work across multiple systems flake-utils.url = "github:numtide/flake-utils"; @@ -30,10 +30,12 @@ # Core development tools needed for LISA packages = with pkgs; [ awscli2 # AWS command-line interface for deployment and management + gnumake jq # JSON processor for parsing AWS responses and configuration pre-commit # Git hook framework for code quality checks - python3 # Python runtime for LISA backend services + python311Full # Python runtime for LISA backend services nodejs # Node.js runtime for CDK infrastructure and frontend tooling + nodePackages.aws-cdk # AWS CDK CLI, the command line tool for CDK apps uv # Fast Python package installer and virtual environment manager yq # YAML processor for configuration management ]; From e4b3565d5c4b0279de19a79e2a9acb380e800e65 Mon Sep 17 00:00:00 2001 From: Dustin Sweigart Date: Mon, 27 Oct 2025 13:01:55 -0400 Subject: [PATCH 09/21] Improve MCP Workbench UX with tool validation, error display, and theme support --- .pre-commit-config.yaml | 2 +- Makefile | 4 +- eslint.config.mjs | 36 +- lambda/mcp_workbench/lambda_functions.py | 45 ++ lambda/mcp_workbench/mcp_mocks.py | 97 +++ lambda/mcp_workbench/syntax_validator.py | 301 +++++++++ lib/serve/mcpWorkbenchConstruct.ts | 13 +- lib/stages.ts | 23 + lib/user-interface/react/package.json | 1 + lib/user-interface/react/src/App.tsx | 231 +++---- .../react/src/components/Topbar.tsx | 35 +- .../McpWorkbenchManagementComponent.tsx | 362 ++++++++--- .../src/shared/color-scheme.provider.tsx | 21 + .../src/shared/modal/confirmation-modal.tsx | 13 +- .../react/src/shared/model/mcp-tools.model.ts | 22 + .../src/shared/reducers/mcp-tools.reducer.ts | 13 +- .../react/src/shared/util/hooks.ts | 21 +- package-lock.json | 22 +- package.json | 2 + requirements-dev.txt | 2 +- test/lambda/test_syntax_validator.py | 595 ++++++++++++++++++ test_enhanced_validation.py | 177 ++++++ test_simplified_validation.py | 102 +++ 23 files changed, 1876 insertions(+), 264 deletions(-) create mode 100644 lambda/mcp_workbench/mcp_mocks.py create mode 100644 lambda/mcp_workbench/syntax_validator.py create mode 100644 lib/user-interface/react/src/shared/color-scheme.provider.tsx create mode 100644 test/lambda/test_syntax_validator.py create mode 100644 test_enhanced_validation.py create mode 100644 test_simplified_validation.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef1b8204e..f68be4d2d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: bandit args: [--recursive, -c=pyproject.toml] - additional_dependencies: ['bandit[toml]', 'pbr'] + additional_dependencies: ['bandit[toml]', 'pbr', 'PyYAML'] - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 diff --git a/Makefile b/Makefile index f61a501bd..ed8cd4257 100644 --- a/Makefile +++ b/Makefile @@ -96,8 +96,8 @@ DEPLOYMENT_STAGE := prod endif # ACCOUNT_NUMBERS_ECR - AWS account numbers that need to be logged into with Docker CLI to use ECR -ifneq ($(shell cat $(PROJECT_DIR)/config-custom.yaml | yq '.accountNumbersEcr'), null) -ACCOUNT_NUMBERS_ECR := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .accountNumbersEcr[]) +ifneq ($(shell yq '.accountNumbersEcr' $(PROJECT_DIR)/config-custom.yaml), null) +ACCOUNT_NUMBERS_ECR := $(shell yq '.accountNumbersEcr[]' $(PROJECT_DIR)/config-custom.yaml) endif # Append deployed account number to array for dockerLogin rule diff --git a/eslint.config.mjs b/eslint.config.mjs index 520d3abf2..a24b7ea89 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -142,30 +142,32 @@ export default [ }, { ignores: [ - 'dist/**', - 'node_modules/**', + '**/*.bundle.js', + '**/*.d.ts', + '**/*.min.js', + '**/.venv/**', + '**/build/**', + '**/coverage/**', + '**/dist/**', + '**/venv/**', + '*.bundle.js', + '*.min.js', + '.venv/**', 'build/**', 'coverage/**', + 'cypress/dist/**', + 'dist/**', + 'ecs_model_deployer/dist/**', 'htmlcov/**', + 'lib/docs/.vitepress/cache/**', + 'lib/docs/dist/**', 'lib/user-interface/react/dist/**', 'lib/user-interface/react/public/**', - 'lib/docs/dist/**', - 'lib/docs/.vitepress/cache/**', - 'ecs_model_deployer/dist/**', + 'node_modules/**', + 'pnpm-lock.yaml', + 'pnpm-workspace.yaml', 'vector_store_deployer/dist/**', - 'cypress/dist/**', - '.venv/**', 'venv/**', - '*.min.js', - '*.bundle.js', - '**/*.min.js', - '**/*.bundle.js', - '**/dist/**', - '**/build/**', - '**/coverage/**', - '**/.venv/**', - '**/venv/**', - '**/*.d.ts' ] } ]; diff --git a/lambda/mcp_workbench/lambda_functions.py b/lambda/mcp_workbench/lambda_functions.py index a1603d906..3ccc8699c 100644 --- a/lambda/mcp_workbench/lambda_functions.py +++ b/lambda/mcp_workbench/lambda_functions.py @@ -28,6 +28,8 @@ from utilities.common_functions import api_wrapper, retry_config from utilities.exceptions import HTTPException +from .syntax_validator import PythonSyntaxValidator + logger = logging.getLogger(__name__) # Initialize the S3 resource using environment variables @@ -255,3 +257,46 @@ def delete(event: dict, context: dict) -> Dict[str, str]: except Exception as e: logger.error("Unexpected error deleting tool: %s", e, exc_info=True) raise ValueError(f"Failed to delete tool: {e}") from e + + +@api_wrapper +def validate_syntax(event: dict, context: dict) -> Dict[str, Any]: + """Validate Python code syntax without execution.""" + if not is_admin(event): + raise ValueError("Only admin users can validate code syntax.") + + try: + body = json.loads(event["body"], parse_float=Decimal) + + # Ensure the required field is present + if "code" not in body: + raise ValueError("Missing required field: 'code' is required.") + + code = body["code"] + if not isinstance(code, str): + raise ValueError("Code must be a string.") + + logger.info("Validating Python code syntax") + + # Initialize the validator and validate the code + validator = PythonSyntaxValidator() + result = validator.validate_code(code) + + # Convert the dataclass to a dictionary for JSON serialization + response = { + "is_valid": result.is_valid, + "syntax_errors": result.syntax_errors, + "missing_required_imports": result.missing_required_imports, + "validation_timestamp": datetime.now().isoformat(), + } + + logger.info(f"Validation completed. Valid: {result.is_valid}, " f"Errors: {len(result.syntax_errors)}") + + return response + + except json.JSONDecodeError as e: + logger.error("Invalid JSON in request body: %s", e, exc_info=True) + raise ValueError(f"Invalid request body: {e}") from e + except Exception as e: + logger.error("Unexpected error validating syntax: %s", e, exc_info=True) + raise ValueError(f"Failed to validate syntax: {e}") from e diff --git a/lambda/mcp_workbench/mcp_mocks.py b/lambda/mcp_workbench/mcp_mocks.py new file mode 100644 index 000000000..a500d5756 --- /dev/null +++ b/lambda/mcp_workbench/mcp_mocks.py @@ -0,0 +1,97 @@ +# 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. + +"""Mock implementations of MCP Workbench core components for validation purposes. + +These mocks are used by the syntax validator to allow user code to import +and use MCP Workbench constructs without needing the full MCP Workbench +package installed. They provide just enough functionality to validate +the structure and usage of MCP tools. +""" + +from abc import ABC, abstractmethod +from functools import wraps +from typing import Any, Callable + + +class BaseTool(ABC): + """ + Mock BaseTool for validation purposes. + + This provides the same interface as the real BaseTool class, + allowing validation of class-based MCP tools without requiring + the full MCP Workbench package. + """ + + def __init__(self, name: str, description: str): + """ + Initialize the tool with required metadata. + + Args: + name: The name of the tool + description: A description of what the tool does + """ + self.name = name + self.description = description + + @abstractmethod + async def execute(self) -> Callable[..., Any]: + """ + Returns a function to be executed as the tool. + + Returns: + The function to be executed + """ + pass + + +def mcp_tool(name: str, description: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """ + Mock mcp_tool decorator for validation purposes. + + This provides the same interface as the real mcp_tool decorator, + allowing validation of function-based MCP tools without requiring + the full MCP Workbench package. + + Args: + name: The name of the tool + description: A description of what the tool does + + Returns: + The decorated function with MCP tool metadata + """ + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + # Store metadata as function attributes + func._mcp_tool_name = name # type: ignore[attr-defined] + func._mcp_tool_description = description # type: ignore[attr-defined] + func._is_mcp_tool = True # type: ignore[attr-defined] + + @wraps(func) + async def wrapper(*args: Any, **kwargs: Any) -> Any: + # If the function is already async, await it + if hasattr(func, "__code__") and func.__code__.co_flags & 0x80: # CO_COROUTINE + return await func(*args, **kwargs) + else: + return func(*args, **kwargs) + + # Copy metadata to wrapper + wrapper._mcp_tool_name = name # type: ignore[attr-defined] + wrapper._mcp_tool_description = description # type: ignore[attr-defined] + wrapper._is_mcp_tool = True # type: ignore[attr-defined] + wrapper._original_func = func # type: ignore[attr-defined] + + return wrapper + + return decorator diff --git a/lambda/mcp_workbench/syntax_validator.py b/lambda/mcp_workbench/syntax_validator.py new file mode 100644 index 000000000..50f6afa83 --- /dev/null +++ b/lambda/mcp_workbench/syntax_validator.py @@ -0,0 +1,301 @@ +# 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. + +"""Python syntax validation module for MCP Workbench.""" +import ast +import importlib.util +import logging +import os +import sys +from dataclasses import dataclass +from types import ModuleType +from typing import Any, Dict, List, Optional + +logger = logging.getLogger(__name__) + + +@dataclass +class ValidationResult: + """Result of Python code validation.""" + + is_valid: bool + syntax_errors: List[Dict[str, Any]] + missing_required_imports: Optional[List[str]] = None + + def __post_init__(self) -> None: + """Initialize list fields if None.""" + if self.missing_required_imports is None: + self.missing_required_imports = [] + + +class PythonSyntaxValidator: + """Validates Python code syntax and imports without execution.""" + + # Required MCP Workbench imports + REQUIRED_MCP_IMPORTS = [("mcpworkbench.core.annotations", "mcp_tool"), ("mcpworkbench.core.base_tool", "BaseTool")] + + def __init__(self) -> None: + """Initialize the validator.""" + # Safety limits + self.max_code_size = 100_000 # 100KB + + def validate_code(self, code: str) -> ValidationResult: + """ + Validate Python code for syntax and required imports. + + Args: + code: Python code string to validate + + Returns: + ValidationResult with validation details + """ + syntax_errors = [] + + # Safety checks + if len(code) > self.max_code_size: + return ValidationResult( + is_valid=False, + syntax_errors=[ + { + "type": "CodeTooLarge", + "message": ( + f"Code size ({len(code)} bytes) exceeds maximum allowed ({self.max_code_size} bytes)" + ), + "line": 0, + "column": 0, + } + ], + missing_required_imports=[], + ) + + # Basic input validation + if not code or not code.strip(): + return ValidationResult( + is_valid=False, + syntax_errors=[{"type": "EmptyCode", "message": "Code cannot be empty", "line": 0, "column": 0}], + missing_required_imports=[], + ) + + # 1. AST-based syntax validation (fast check) + try: + tree = ast.parse(code) + logger.info("AST parsing successful") + except SyntaxError as e: + syntax_errors.append(self._format_syntax_error(e)) + logger.warning(f"Syntax error: {e}") + + # Return early if syntax is invalid + return ValidationResult(is_valid=False, syntax_errors=syntax_errors, missing_required_imports=[]) + except Exception as e: + syntax_errors.append( + {"type": "ParseError", "message": f"Failed to parse code: {str(e)}", "line": 0, "column": 0} + ) + logger.error(f"Parse error: {e}") + return ValidationResult(is_valid=False, syntax_errors=syntax_errors, missing_required_imports=[]) + + # 2. Module execution validation (comprehensive check) + execution_errors = self._validate_module_execution(code) + syntax_errors.extend(execution_errors) + + # 3. Check for required MCP imports + missing_required_imports = self._check_required_mcp_imports(tree) + + # Determine overall validity + is_valid = len(syntax_errors) == 0 and len(missing_required_imports) == 0 + + return ValidationResult( + is_valid=is_valid, syntax_errors=syntax_errors, missing_required_imports=missing_required_imports + ) + + def _validate_module_execution(self, code: str) -> List[Dict[str, Any]]: + """Validate code by attempting to execute it as a module.""" + errors = [] + + try: + # Set up the MCP environment FIRST (inject mocks into sys.modules) + # This must happen before exec() so imports can find the mocks + self._setup_mcp_environment(None) + + # Create a temporary module spec + spec = importlib.util.spec_from_loader("temp_validation_module", loader=None) + if spec is None: + errors.append( + { + "type": "ModuleError", + "message": "Failed to create module spec for validation", + "line": 0, + "column": 0, + } + ) + return errors + + module = importlib.util.module_from_spec(spec) + + # Execute the code in the module context + # The mocks are already in sys.modules so imports will work + exec(code, module.__dict__) # nosec B102 + logger.info("Module execution successful") + + except ImportError as e: + errors.append({"type": "ImportError", "message": str(e), "line": 0, "column": 0}) + logger.warning(f"Import error during execution: {e}") + except SyntaxError as e: + # Shouldn't happen since AST passed, but just in case + errors.append(self._format_syntax_error(e)) + logger.warning(f"Syntax error during execution: {e}") + except NameError as e: + errors.append({"type": "NameError", "message": str(e), "line": 0, "column": 0}) + logger.warning(f"Name error during execution: {e}") + except Exception as e: + errors.append( + {"type": "ExecutionError", "message": f"Error executing code: {str(e)}", "line": 0, "column": 0} + ) + logger.error(f"Execution error: {e}") + + return errors + + def _setup_mcp_environment(self, module: Any) -> None: + """Set up the module with required MCP imports available.""" + # Check if real MCP Workbench is available + if "mcpworkbench.core.base_tool" not in sys.modules: + # Real package not available, inject mocks into sys.modules + logger.info("Real MCP Workbench not found, setting up mocks") + mcp_tool_func: Any = None + base_tool_class: Any = None + + try: + # Try relative import first (when running as part of a package) + from .mcp_mocks import BaseTool as base_tool_class + from .mcp_mocks import mcp_tool as mcp_tool_func + + logger.info("Successfully imported mocks via relative import") + except ImportError as e: + logger.info(f"Relative import failed: {e}, trying absolute import") + try: + # Fall back to absolute import (when running standalone) + import mcp_mocks + + mcp_tool_func = mcp_mocks.mcp_tool + base_tool_class = mcp_mocks.BaseTool + logger.info("Successfully imported mocks via absolute import") + except ImportError as mock_error: + logger.error(f"CRITICAL: Failed to import MCP mocks via both methods: {mock_error}") + logger.error(f"Current directory: {os.getcwd() if 'os' in dir() else 'unknown'}") + logger.error(f"sys.path: {sys.path[:3]}") # Show first 3 paths + return + + # Create mock module hierarchy in sys.modules + # This allows user code to do: from mcpworkbench.core.base_tool import BaseTool + if "mcpworkbench" not in sys.modules: + sys.modules["mcpworkbench"] = ModuleType("mcpworkbench") + + if "mcpworkbench.core" not in sys.modules: + core_module = ModuleType("mcpworkbench.core") + sys.modules["mcpworkbench.core"] = core_module + sys.modules["mcpworkbench"].core = core_module # type: ignore[attr-defined] + + # Create and register the base_tool mock module + base_tool_module = ModuleType("mcpworkbench.core.base_tool") + base_tool_module.BaseTool = base_tool_class # type: ignore[attr-defined] + sys.modules["mcpworkbench.core.base_tool"] = base_tool_module + sys.modules["mcpworkbench.core"].base_tool = base_tool_module # type: ignore[attr-defined] + + # Create and register the annotations mock module + annotations_module = ModuleType("mcpworkbench.core.annotations") + annotations_module.mcp_tool = mcp_tool_func # type: ignore[attr-defined] + sys.modules["mcpworkbench.core.annotations"] = annotations_module + sys.modules["mcpworkbench.core"].annotations = annotations_module # type: ignore[attr-defined] + + logger.info("MCP mock modules successfully injected into sys.modules") + logger.info(f"Modules now in sys.modules: {[k for k in sys.modules.keys() if 'mcpworkbench' in k]}") + else: + logger.info("Real MCP Workbench package is already available in sys.modules") + + def _check_required_mcp_imports(self, tree: ast.AST) -> List[str]: + """Check if required MCP imports are present in the AST.""" + missing_required = [] + + # Collect all imports from the AST + imports = self._collect_imports(tree) + + # Check if at least one required import is present + has_required_import = False + + for module, name in self.REQUIRED_MCP_IMPORTS: + # Check if imported via 'from module import name' + if module in imports["from_imports"] and name in imports["from_imports"][module]: + has_required_import = True + break + + # Check if imported via star import + if module in imports["star_imports"]: + has_required_import = True + break + + if not has_required_import: + missing_required.append("At least one of the required MCP Workbench imports is missing") + + return missing_required + + def _collect_imports(self, tree: ast.AST) -> Dict[str, Any]: + """Collect all import statements from the AST.""" + imports: Dict[str, Any] = { + "modules": set(), # Direct module imports: import os + "from_imports": {}, # From imports: from os import path -> {'os': {'path'}} + "aliases": {}, # Import aliases: import numpy as np -> {'np': 'numpy'} + "star_imports": set(), # Star imports: from os import * + } + + class ImportVisitor(ast.NodeVisitor): + def visit_Import(self, node: ast.Import) -> None: + for alias in node.names: + module_name = alias.name + alias_name = alias.asname or module_name + imports["modules"].add(module_name) + if alias.asname: + imports["aliases"][alias_name] = module_name + self.generic_visit(node) + + def visit_ImportFrom(self, node: ast.ImportFrom) -> None: + if node.module: + module_name = node.module + if node.names and len(node.names) == 1 and node.names[0].name == "*": + # Star import + imports["star_imports"].add(module_name) + else: + # Regular from import + if module_name not in imports["from_imports"]: + imports["from_imports"][module_name] = set() + + for alias in node.names: + name = alias.name + alias_name = alias.asname or name + imports["from_imports"][module_name].add(name) + if alias.asname: + imports["aliases"][alias_name] = f"{module_name}.{name}" + self.generic_visit(node) + + visitor = ImportVisitor() + visitor.visit(tree) + return imports + + def _format_syntax_error(self, syntax_error: SyntaxError) -> Dict[str, Any]: + """Format a SyntaxError into a standardized error dictionary.""" + return { + "type": "SyntaxError", + "message": str(syntax_error.msg) if syntax_error.msg else "Syntax error", + "line": syntax_error.lineno or 0, + "column": syntax_error.offset or 0, + "text": syntax_error.text.strip() if syntax_error.text else "", + } diff --git a/lib/serve/mcpWorkbenchConstruct.ts b/lib/serve/mcpWorkbenchConstruct.ts index 5322e6247..1418b2c33 100644 --- a/lib/serve/mcpWorkbenchConstruct.ts +++ b/lib/serve/mcpWorkbenchConstruct.ts @@ -110,6 +110,13 @@ export default class McpWorkbenchConstruct extends Construct { method: 'DELETE', environment: env, path: 'mcp-workbench/{toolId}' + }, { + name: 'validate_syntax', + resource: 'mcp_workbench', + description: 'Validate Python code syntax', + method: 'POST', + environment: env, + path: 'mcp-workbench/validate-syntax' }]; // Create IAM role for Lambda @@ -148,7 +155,11 @@ export default class McpWorkbenchConstruct extends Construct { authorizer, lambdaRole, ); - if (f.method === 'POST' || f.method === 'PUT') { + + // Grant S3 permissions based on function type + if (['validate_syntax'].includes(f.name)) { + // No S3 permissions needed for syntax validation + } else if (f.method === 'POST' || f.method === 'PUT') { workbenchBucket.grantWrite(lambdaFunction); } else if (f.method === 'GET') { workbenchBucket.grantRead(lambdaFunction); diff --git a/lib/stages.ts b/lib/stages.ts index 45221370e..e66da0d8f 100644 --- a/lib/stages.ts +++ b/lib/stages.ts @@ -29,6 +29,7 @@ import { Tags, } from 'aws-cdk-lib'; import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as events from 'aws-cdk-lib/aws-events'; import { Construct } from 'constructs'; import { AwsSolutionsChecks, NagSuppressions, NIST80053R5Checks } from 'cdk-nag'; @@ -166,6 +167,27 @@ class RemoveEventSourceMappingTagsAspect implements IAspect { } } +/** + * Removes Tags property from all AWS::Events::Rule resources in a CDK application. + * This is required for AWS GovCloud regions which don't support Tags on Rule resources. + */ +class RemoveEventRuleTagsAspect implements IAspect { + /** + * Checks if the given node is an instance of CfnResource and specifically an AWS::Events::Rule resource. + * If true, it removes the Tags property to prevent deployment failures in AWS GovCloud regions. + * + * @param {Construct} node - The CDK construct being visited. + */ + public visit (node: Construct): void { + // Check if the node is a CloudFormation resource of type AWS::Events::Rule + if (node instanceof events.CfnRule) { + // Remove Tags property for AWS GovCloud compatibility + node.addPropertyDeletionOverride('Tags'); + } + } +} + + export type CommonStackProps = { synthesizer?: IStackSynthesizer; } & BaseProps; @@ -434,6 +456,7 @@ export class LisaServeApplicationStage extends Stage { // AWS GovCloud regions don't support Tags on EventSourceMapping resources if (config.region.includes('gov')) { Aspects.of(this).add(new RemoveEventSourceMappingTagsAspect()); + Aspects.of(this).add(new RemoveEventRuleTagsAspect()); } } } diff --git a/lib/user-interface/react/package.json b/lib/user-interface/react/package.json index 2b8121ffc..2769f72f6 100644 --- a/lib/user-interface/react/package.json +++ b/lib/user-interface/react/package.json @@ -36,6 +36,7 @@ "langchain": "^0.3.15", "lodash": "^4.17.21", "luxon": "^3.5.0", + "dompurify": "^3.2.5", "mermaid": "^11.10.1", "react": "^18.3.1", "react-ace": "^14.0.1", diff --git a/lib/user-interface/react/src/App.tsx b/lib/user-interface/react/src/App.tsx index 56356e559..cb4e27b81 100644 --- a/lib/user-interface/react/src/App.tsx +++ b/lib/user-interface/react/src/App.tsx @@ -43,6 +43,8 @@ import { ConfigurationContext } from './shared/configuration.provider'; import McpServers from '@/pages/Mcp'; import ModelComparisonPage from './pages/ModelComparison'; import McpWorkbench from './pages/McpWorkbench'; +import ColorSchemeContext from './shared/color-scheme.provider'; +import { applyMode, Mode } from '@cloudscape-design/global-styles'; export type RouteProps = { @@ -95,6 +97,23 @@ function App () { }); const config = fullConfig?.[0]; + const [colorScheme, setColorScheme] = useState(() => { + // Check to see if Media-Queries are supported + if (window.matchMedia) { + // Check if the dark-mode Media-Query matches + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + // Dark + return Mode.Dark; + } + } + + return Mode.Light; + }); + + useEffect(() => { + applyMode(colorScheme); + }, [colorScheme]); + useEffect(() => { if (nav) { setShowNavigation(true); @@ -104,60 +123,61 @@ function App () { }, [nav]); return ( - - {config?.configuration.systemBanner.isEnabled && } -
- -
- - } - toolsHide={true} - notifications={} - stickyNotifications={true} - navigation={nav} - navigationWidth={450} - content={ - - - - - } - /> - - - - } - /> - - - - } - /> - {config?.configuration?.enabledComponents?.modelLibrary && - - - } - />} - {config?.configuration?.enabledComponents?.showRagLibrary && + + + {config?.configuration.systemBanner.isEnabled && } +
+ +
+ + } + toolsHide={true} + notifications={} + stickyNotifications={true} + navigation={nav} + navigationWidth={450} + content={ + + + + + } + /> + + + + } + /> + + + + } + /> + {config?.configuration?.enabledComponents?.modelLibrary && + + + } + />} + {config?.configuration?.enabledComponents?.showRagLibrary && <> } - {config?.configuration?.enabledComponents?.showPromptTemplateLibrary && - - - } - />} - - - - } - /> - {config?.configuration?.enabledComponents?.mcpConnections && - - - } - />} - {config?.configuration?.enabledComponents?.showMcpWorkbench && + {config?.configuration?.enabledComponents?.showPromptTemplateLibrary && + + + } + />} + + + } + /> + {config?.configuration?.enabledComponents?.mcpConnections && + + + } + />} + {config?.configuration?.enabledComponents?.showMcpWorkbench && + + } + /> + } + {config?.configuration?.enabledComponents?.enableModelComparisonUtility && + + + } /> - } - {config?.configuration?.enabledComponents?.enableModelComparisonUtility && - - } - /> - } - - - Loading configuration... - - : - - } /> - - } - /> - {confirmationModal && } - {config?.configuration.systemBanner.isEnabled && } -
+ + + Loading configuration... + + : + + } /> +
+ } + /> + {confirmationModal && } + {config?.configuration.systemBanner.isEnabled && } +
+ ); } diff --git a/lib/user-interface/react/src/components/Topbar.tsx b/lib/user-interface/react/src/components/Topbar.tsx index 90fb48633..2e246379e 100644 --- a/lib/user-interface/react/src/components/Topbar.tsx +++ b/lib/user-interface/react/src/components/Topbar.tsx @@ -14,16 +14,17 @@ limitations under the License. */ -import { ReactElement, useEffect, useState } from 'react'; +import { ReactElement, useContext } from 'react'; import { useAuth } from 'react-oidc-context'; import { useHref, useNavigate } from 'react-router-dom'; -import { applyDensity, applyMode, Density, Mode } from '@cloudscape-design/global-styles'; +import { applyDensity, Density, Mode } from '@cloudscape-design/global-styles'; import TopNavigation, { TopNavigationProps } from '@cloudscape-design/components/top-navigation'; import { getBaseURI } from './utils'; import { purgeStore, useAppSelector } from '../config/store'; import { selectCurrentUserIsAdmin, selectCurrentUsername } from '../shared/reducers/user.reducer'; import { IConfiguration } from '../shared/model/configuration.model'; import { ButtonDropdownProps } from '@cloudscape-design/components'; +import ColorSchemeContext from '@/shared/color-scheme.provider'; applyDensity(Density.Comfortable); @@ -36,31 +37,7 @@ function Topbar ({ configs }: TopbarProps): ReactElement { const auth = useAuth(); const isUserAdmin = useAppSelector(selectCurrentUserIsAdmin); const userName = useAppSelector(selectCurrentUsername); - const [isDarkMode, setIsDarkMode] = useState(window.matchMedia('(prefers-color-scheme: dark)').matches); - - useEffect(() => { - if (isDarkMode) { - applyMode(Mode.Dark); - } else { - applyMode(Mode.Light); - } - }, [isDarkMode]); - - useEffect(() => { - // Check to see if Media-Queries are supported - if (window.matchMedia) { - // Check if the dark-mode Media-Query matches - if (window.matchMedia('(prefers-color-scheme: dark)').matches) { - // Dark - applyMode(Mode.Dark); - } else { - // Light - applyMode(Mode.Light); - } - } else { - // Default (when Media-Queries are not supported) - } - }, []); + const {colorScheme, setColorScheme} = useContext(ColorSchemeContext); const libraryItems = [ ...(configs?.configuration.enabledComponents?.modelLibrary ? [{ @@ -183,7 +160,7 @@ function Topbar ({ configs }: TopbarProps): ReactElement { await auth.signoutSilent(); break; case 'color-mode': - setIsDarkMode(!isDarkMode); + setColorScheme(colorScheme === Mode.Light ? Mode.Dark : Mode.Light); break; default: break; @@ -192,7 +169,7 @@ function Topbar ({ configs }: TopbarProps): ReactElement { iconName: 'user-profile', items: [ { id: 'version-info', text: `LISA v${window.gitInfo?.revisionTag}`, disabled: true }, - { id: 'color-mode', text: isDarkMode ? 'Light mode' : 'Dark mode', iconSvg: ( + { id: 'color-mode', text: colorScheme === Mode.Light ? 'Dark mode' : 'Light mode', iconSvg: ( dict:\n \'\'\'\n Perform basic arithmetic operations using the decorator approach.\n \n The @mcp_tool decorator automatically:\n 1. Registers the function as an MCP tool\n 2. Extracts parameter information from type annotations\n 3. Uses the Annotated descriptions for parameter documentation\n 4. Handles the MCP protocol communication\n \n This approach is ideal for:\n - Simple, stateless operations\n - Quick prototyping\n - Tools that don\'t need complex initialization\n \'\'\'\n \n if operator == "add":\n result = left_operand + right_operand\n elif operator == "subtract":\n result = left_operand - right_operand\n elif operator == "multiply":\n result = left_operand * right_operand\n elif operator == "divide":\n if right_operand == 0:\n raise ValueError("Cannot divide by zero")\n result = left_operand / right_operand\n else:\n raise ValueError(f"Unknown operator: {operator}")\n \n return {\n "operator": operator,\n "left_operand": left_operand,\n "right_operand": right_operand,\n "result": result\n }\n\n\n# =============================================================================\n# METHOD 2: CLASS-BASED APPROACH\n# =============================================================================\n# This is the more structured approach, ideal for complex tools that need\n# initialization, state management, or multiple related operations.\n\nclass CalculatorTool(BaseTool):\n """\n A simple calculator tool that performs basic arithmetic operations.\n \n This class demonstrates the class-based approach to creating MCP tools:\n 1. Inherit from BaseTool\n 2. Initialize with name and description in __init__\n 3. Implement execute() method that returns the callable function\n 4. Define the actual tool function with proper type annotations\n """\n \n def __init__(self):\n """\n Initialize the tool with metadata.\n \n The BaseTool constructor requires:\n - name: A unique identifier for the tool\n - description: A clear description of what the tool does\n """\n super().__init__(\n name="calculator",\n description="Performs basic arithmetic operations (add, subtract, multiply, divide)"\n )\n\n async def execute(self):\n """\n Return the callable function that implements the tool\'s functionality.\n \n This method is called by the MCP framework to get the actual function\n that will be executed when the tool is invoked.\n """\n return self.calculate\n \n async def calculate(\n self,\n operator: Annotated[str, "add, subtract, multiply, or divide"],\n left_operand: Annotated[float, "The first number"],\n right_operand: Annotated[float, "The second number"]\n ):\n """\n Execute the calculator operation.\n \n Parameter Type Annotations with Context:\n =======================================\n Notice the use of Annotated[type, "description"] for each parameter.\n This is OPTIONAL but highly recommended because it provides:\n \n 1. Type information for the MCP framework\n 2. Human-readable descriptions that help AI models understand\n what each parameter is for\n 3. Better error messages and validation\n \n The Annotated type comes from typing module and follows this pattern:\n Annotated[actual_type, "description_string"]\n \n Examples:\n - Annotated[str, "The operation to perform"]\n - Annotated[int, "A positive integer between 1 and 100"]\n - Annotated[list[str], "A list of file paths to process"]\n """ \n if operator == "add":\n result = left_operand + right_operand\n elif operator == "subtract":\n result = left_operand - right_operand\n elif operator == "multiply":\n result = left_operand * right_operand\n elif operator == "divide":\n if right_operand == 0:\n raise ValueError("Cannot divide by zero")\n result = left_operand / right_operand\n else:\n raise ValueError(f"Unknown operator: {operator}")\n \n return {\n "operator": operator,\n "left_operand": left_operand,\n "right_operand": right_operand,\n "result": result\n }'; @@ -47,29 +49,35 @@ export function McpWorkbenchManagementComponent (): ReactElement { // API hooks const { data: tools = [], isFetching: isLoadingTools, refetch } = useListMcpToolsQuery(); const [selectedToolId, setSelectedToolId] = useState(null); - const { data: selectedToolData, isFetching: isLoadingTool, } = useGetMcpToolQuery(selectedToolId!, { + const { data: selectedToolData, isUninitialized } = useGetMcpToolQuery(selectedToolId!, { + skip: selectedToolId === null, refetchOnMountOrArgChange: true, refetchOnFocus: true }); + const [loadingAce, setLoadingAce] = useState(true); + const [isDirty, setIsDirty] = useState(false); const [createToolMutation, { isLoading: isCreating }] = useCreateMcpToolMutation(); const [updateToolMutation, { isLoading: isUpdating }] = useUpdateMcpToolMutation(); const [deleteToolMutation] = useDeleteMcpToolMutation(); - - const [isDirty, setIsDirty] = useState(false); + const {colorScheme} = useContext(ColorSchemeContext); + const [statusText, setStatusText] = useState(''); const schema = z.object({ id: z.string().regex(/^[a-z0-9_.]+?(\.py)?$/).trim().min(3, 'String cannot be empty.'), contents: z.string().trim().min(1, 'String cannot be empty.'), }); - const { errors, touchFields, setFields, isValid, state } = useValidationReducer(schema, { - form: { id: `my_new_tool_${Date.now()}`, contents: DEFAULT_CONTENT} as IMcpTool, + const { errors, touchFields, setFields, isValid, state, setState } = useValidationReducer(schema, { + form: { } as Partial, formSubmitting: false, touched: {}, validateAll: false }); + const [validateMcpToolMutation, {isLoading: isLoadingValidateMcpTool, data: validMcpToolResponse} ] = useValidateMcpToolMutation(); + const [editor, setEditor] = useState(); + // Filtering and pagination state const [filterText, setFilterText] = useState(''); const [currentPageIndex, setCurrentPageIndex] = useState(1); @@ -87,6 +95,83 @@ export function McpWorkbenchManagementComponent (): ReactElement { currentPageIndex * pageSize ); + const [ waitingForValidation, setWaitingForValidation ] = useState(false); + + // Dont validate immediately, wait until this hasn't been called for 300ms + const debouncedValidation = useDebounce(useCallback((contents: string) => { + validateMcpToolMutation(contents).then((response) => { + setWaitingForValidation(false); + setStatusText(undefined); + + // Handle validation response + if ('data' in response) { + // Successful validation response + const validationData = response.data; + const annotations = []; + + // Add syntax error annotations + if (validationData.syntax_errors && validationData.syntax_errors.length > 0) { + validationData.syntax_errors.forEach((error) => { + annotations.push({ + row: Math.max(0, error.line - 1), // Ace editor is 0-indexed + column: error.column, + type: 'error', + text: `${error.type}: ${error.message}` + }); + }); + } + + if (validationData.missing_required_imports.length > 0) { + validationData.missing_required_imports.forEach((error) => { + annotations.push({ + row: 0, + column: 0, + type: 'error', + text: error + }); + }); + } + + // Apply annotations to editor + if (editor) { + editor.getSession().setAnnotations(annotations); + } + + } else if ('error' in response) { + // Error response from validation API + console.error('Validation API error:', response.error); + + // Clear any existing annotations + if (editor) { + editor.getSession().setAnnotations([]); + } + + // Show error notification + const errorMessage = 'data' in response.error && response.error.data?.message + ? response.error.data.message + : 'Unknown validation error'; + notificationService.generateNotification( + `Validation error: ${errorMessage}`, + 'error' + ); + } + }).catch((error) => { + // Handle promise rejection + console.error('Validation request failed:', error); + + // Clear any existing annotations + if (editor) { + editor.getSession().setAnnotations([]); + } + + // Show error notification + notificationService.generateNotification( + `Validation failed: ${error.message || 'Unknown error'}`, + 'error' + ); + }); + }, [validateMcpToolMutation, editor, notificationService]), 300); + // remove top breadcrumbs dispatch(setBreadcrumbs([])); @@ -95,83 +180,109 @@ export function McpWorkbenchManagementComponent (): ReactElement { setCurrentPageIndex(1); }, [filterText]); + useEffect(() => { + async function loadAce () { + await import('ace-builds'); + + // Import language modes you need + await import('ace-builds/src-noconflict/mode-python'); + + // Import themes + await import('ace-builds/src-noconflict/theme-cloud_editor'); + await import('ace-builds/src-noconflict/theme-cloud_editor_dark'); + + setLoadingAce(false); + } + + loadAce(); + }, []); + // Update editor content when a tool is selected useEffect(() => { - if (selectedToolId !== null && selectedToolData && isDirty === false) { + if (!isUninitialized && selectedToolData?.id) { setFields({ id: selectedToolData.id, contents: selectedToolData.contents, size: selectedToolData.size, - updated_at : selectedToolData.updated_at + updated_at: selectedToolData.updated_at }); - setIsDirty(true); + setIsDirty(false); + setStatusText(undefined); } - }, [selectedToolId, selectedToolData, setFields, isDirty]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isUninitialized, selectedToolData]); // Handle tool selection const handleToolSelect = (tool: IMcpTool) => { if (isDirty) { dispatch( setConfirmationModal({ - action: 'Switch Tool?', - resourceName: 'Unsaved change', + title: 'Switch Tool?', + action: 'Switch Tool', onConfirm: () => { setSelectedToolId(tool.id); - setIsDirty(false); + setStatusText('Loading MCP tool.'); }, - description: 'You have unsaved changes. Switching tools will lose these changes.' + description: 'You have unsaved changes. Switching tools will discard these changes.' }) ); } else { setSelectedToolId(tool.id); + setStatusText('Loading MCP tool.'); } }; - // Handle editor content change - const handleEditorChange = (value: string) => { - setFields({ - contents: value - }); - setIsDirty(true); - touchFields(['contents']); - }; - // Handle creating new tool - const handleCreateNew = () => { + const handleCreateNew = (event: CancellableEventHandler) => { + event.preventDefault(); + const newTool = { - id: ['my_new_tool', Date.now()].join('-'), + id: '', contents: DEFAULT_CONTENT, }; if (isDirty && selectedToolId) { dispatch( setConfirmationModal({ + title: 'Create New Tool?', action: 'Create New Tool', - resourceName: '', onConfirm: () => { setSelectedToolId(null); + touchFields(['id'], ModifyMethod.Unset); setFields(newTool); setIsDirty(false); }, - description: 'You have unsaved changes. Creating a new tool will lose these changes.' + description: 'You have unsaved changes. Creating a new tool will discard these changes.' }) ); } else { setSelectedToolId(null); + touchFields(['id'], ModifyMethod.Unset); setFields(newTool); - setIsDirty(true); + setIsDirty(false); } }; // Handle create tool const handleCreateTool = async () => { + const result = schema.safeParse(state.form); + if (!result.success) { + setState({ + ...state, + validateAll: true + }); + + return; + } + try { - await createToolMutation({ + const result = await createToolMutation({ id: state.form.id, contents: state.form.contents }).unwrap(); notificationService.generateNotification(`Successfully created tool: ${state.form.id}`, 'success'); + setSelectedToolId(result.id); setIsDirty(false); dispatch(mcpToolsApi.util.invalidateTags(['mcpTools'])); refetch(); @@ -218,7 +329,7 @@ export function McpWorkbenchManagementComponent (): ReactElement { id: '', contents: '', size: undefined, - updated_at : undefined + updated_at: undefined }); setIsDirty(false); } @@ -234,6 +345,14 @@ export function McpWorkbenchManagementComponent (): ReactElement { ); }; + const disabled = !isDirty || !isValid || (isLoadingValidateMcpTool || waitingForValidation) || !validMcpToolResponse?.is_valid; + const disabledReason = [ + {predicate: !isDirty, message: 'Tool has not been modified.'}, + {predicate: !isValid, message: 'Ensure all fields are valid.'}, + {predicate: isLoadingValidateMcpTool || waitingForValidation, message: 'Validating tool.'}, + {predicate: !validMcpToolResponse?.is_valid, message: 'Please correct all errors.'} + ].find((reason) => reason.predicate)?.message; + return ( */}
+ { + setFields({ + contents + }); + debouncedValidation(contents); + setStatusText('Validating MCP tool.'); + setIsDirty(true); + touchFields(['contents']); + + if (!waitingForValidation) { + setWaitingForValidation(true); + } + }} + onLoad={(editor) => { + setEditor(editor); + }} + width='100%' + /> +
+ + + + + { statusText ? +

{statusText}

+
: null} + + + {selectedToolId === null ? ( + + ) : ( + + )} +
+
+ :
+ +

Select an existing tool or Create Tool

+ {statusText ? +

{statusText}

+
: null} +
+ +
} ); diff --git a/lib/user-interface/react/src/shared/color-scheme.provider.tsx b/lib/user-interface/react/src/shared/color-scheme.provider.tsx new file mode 100644 index 000000000..a0900a9b2 --- /dev/null +++ b/lib/user-interface/react/src/shared/color-scheme.provider.tsx @@ -0,0 +1,21 @@ +/** + 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 { createContext } from 'react'; + +export const ColorSchemeContext = createContext(null); + +export default ColorSchemeContext; diff --git a/lib/user-interface/react/src/shared/modal/confirmation-modal.tsx b/lib/user-interface/react/src/shared/modal/confirmation-modal.tsx index 2ebdfe48c..949cc34c6 100644 --- a/lib/user-interface/react/src/shared/modal/confirmation-modal.tsx +++ b/lib/user-interface/react/src/shared/modal/confirmation-modal.tsx @@ -23,13 +23,14 @@ export type CallbackFunction = (props?: T) => R; export type ConfirmationModalProps = { action: string; - resourceName: string; + resourceName?: string; onConfirm: () => void; postConfirm?: CallbackFunction; description?: string | ReactElement; disabled?: boolean; onDismiss?: (event?: NonCancelableCustomEvent) => void; ignoreResponses?: boolean; + title?: string }; function ConfirmationModal ({ @@ -40,11 +41,17 @@ function ConfirmationModal ({ description, disabled, onDismiss, - ignoreResponses + ignoreResponses, + title }: ConfirmationModalProps): ReactElement { const [processing, setProcessing] = useState(false); const dispatch = useAppDispatch(); + const headerText = title || [ + action, + resourceName ? `"${resourceName}"` : '' + ].join(' '); + return ( { @@ -88,7 +95,7 @@ function ConfirmationModal ({ } - header={`${action} "${resourceName}"`} + header={headerText} > {description} diff --git a/lib/user-interface/react/src/shared/model/mcp-tools.model.ts b/lib/user-interface/react/src/shared/model/mcp-tools.model.ts index bc7fe4241..bd4b01240 100644 --- a/lib/user-interface/react/src/shared/model/mcp-tools.model.ts +++ b/lib/user-interface/react/src/shared/model/mcp-tools.model.ts @@ -61,6 +61,28 @@ export type IMcpToolDeleteResponse = { message: string; }; +/** + * Interface for MCP tool validation response + */ +export type IMcpToolValidationResponse = { + /** Whether the code is valid */ + is_valid: boolean; + + /** List of syntax errors */ + syntax_errors: Array<{ + type: string; + message: string; + line: number; + column: number; + text?: string; + }>; + /** Missing required MCP imports */ + missing_required_imports: string[]; + + /** Timestamp of validation */ + validation_timestamp: string; +}; + /** * Default empty MCP tool for forms */ diff --git a/lib/user-interface/react/src/shared/reducers/mcp-tools.reducer.ts b/lib/user-interface/react/src/shared/reducers/mcp-tools.reducer.ts index 5d1c94410..5d41461b1 100644 --- a/lib/user-interface/react/src/shared/reducers/mcp-tools.reducer.ts +++ b/lib/user-interface/react/src/shared/reducers/mcp-tools.reducer.ts @@ -22,7 +22,8 @@ import { IMcpToolListResponse, IMcpToolRequest, IMcpToolUpdateRequest, - IMcpToolDeleteResponse + IMcpToolDeleteResponse, + IMcpToolValidationResponse } from '../model/mcp-tools.model'; export const mcpToolsApi = createApi({ @@ -72,6 +73,13 @@ export const mcpToolsApi = createApi({ }), transformErrorResponse: (baseQueryReturnValue) => normalizeError('Delete MCP Tool', baseQueryReturnValue), invalidatesTags: ['mcpTools'], + }), + validateMcpTool: builder.mutation({ + query: (code) => ({ + url: '/mcp-workbench/validate-syntax', + method: 'POST', + data: {code} + }) }) }), }); @@ -82,5 +90,6 @@ export const { useLazyGetMcpToolQuery, useCreateMcpToolMutation, useUpdateMcpToolMutation, - useDeleteMcpToolMutation + useDeleteMcpToolMutation, + useValidateMcpToolMutation, } = mcpToolsApi; diff --git a/lib/user-interface/react/src/shared/util/hooks.ts b/lib/user-interface/react/src/shared/util/hooks.ts index d789b549e..3f2289340 100644 --- a/lib/user-interface/react/src/shared/util/hooks.ts +++ b/lib/user-interface/react/src/shared/util/hooks.ts @@ -15,8 +15,9 @@ */ import { Action, ThunkDispatch } from '@reduxjs/toolkit'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import NotificationService from '../notification/notification.service'; +import { debounce, DebouncedFunc } from 'lodash'; /** * Creates a memoized NotificationService based on {@link dispatch} @@ -26,3 +27,21 @@ export function useNotificationService ( ): ReturnType { return useMemo(() => NotificationService(dispatch), [dispatch]); } + +/** + * Creates a debounced function that delays invoking {@link callback} until after {@link delay} milliseconds have elapsed since + * the last time the debounced function was invoked. + * + * NOTE: The returned function has {@link callback} as a dependency so it is up to the caller to ensure {@link callback} doesn't + * change or is memoized. + * + * @param {Function} callback The function to debounce. + * @param {number} delay The number of milliseconds to delay. + * @returns {Function} The memoized and debounced function. + */ +export function useDebounce void> (callback: T, delay = 300): DebouncedFunc { + // useMemo is necessary because useCallback doesn't understand the dependencies for the debounced function + const debounced = useMemo(() => debounce(callback, delay), [callback, delay]); + + return useCallback(debounced, [debounced]); +} diff --git a/package-lock.json b/package-lock.json index f85ce83b2..9e50a8564 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,9 +32,11 @@ "@aws-sdk/client-iam": "^3.738.0", "@aws-sdk/client-ssm": "^3.738.0", "@cdklabs/cdk-enterprise-iac": "^0.0.512", + "@eslint/js": "^9.38.0", "@stylistic/eslint-plugin": "^5.4.0", "@types/jest": "^29.5.14", "@types/js-yaml": "^4.0.9", + "@types/linkify-it": "^5.0.0", "@types/lodash": "^4.17.15", "@types/node": "^22.13.4", "@types/readline-sync": "^1.4.8", @@ -119,6 +121,7 @@ "@swc/core": "^1.11.8", "ace-builds": "^1.43.2", "axios": "^1.8.2", + "dompurify": "^3.2.5", "fdir": "^6.5.0", "git-repo-info": "^2.1.1", "jszip": "^3.10.1", @@ -3275,9 +3278,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", - "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", "dev": true, "license": "MIT", "engines": { @@ -10862,6 +10865,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", diff --git a/package.json b/package.json index 9b5fc57be..ed3b1ddd1 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "generateSchemaDocs": "npx zod2md -c ./lib/zod2md.config.ts && npx zod2md -c ./lib/zod2md.rag.ts" }, "devDependencies": { + "@eslint/js": "^9.38.0", "@aws-cdk/aws-lambda-python-alpha": "2.125.0-alpha.0", "@aws-sdk/client-iam": "^3.738.0", "@aws-sdk/client-ssm": "^3.738.0", @@ -70,6 +71,7 @@ "globals": "^16.3.0", "husky": "^9.1.7", "jest": "^29.7.0", + "@types/linkify-it": "^5.0.0", "lint-staged": "^15.4.3", "readline-sync": "^1.4.10", "ts-jest": "^29.2.5", diff --git a/requirements-dev.txt b/requirements-dev.txt index c9d32fece..8be923df5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -pyarrow==17.0.0 +pyarrow==20.0.0 datasets==2.20.0 fastapi==0.111.1 fastapi_utils==0.8.0 diff --git a/test/lambda/test_syntax_validator.py b/test/lambda/test_syntax_validator.py new file mode 100644 index 000000000..c1a619c02 --- /dev/null +++ b/test/lambda/test_syntax_validator.py @@ -0,0 +1,595 @@ +# 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. + +"""Comprehensive unit tests for the syntax_validator module.""" + +import ast +import sys +from types import ModuleType +from unittest.mock import patch + +import pytest +from mcp_workbench.syntax_validator import PythonSyntaxValidator, ValidationResult + + +class TestValidationResult: + """Test cases for ValidationResult dataclass.""" + + def test_initialization_with_all_fields(self): + """Test ValidationResult initialization with all fields provided.""" + result = ValidationResult( + is_valid=True, syntax_errors=[{"type": "Error", "message": "test"}], missing_required_imports=["import1"] + ) + assert result.is_valid is True + assert len(result.syntax_errors) == 1 + assert len(result.missing_required_imports) == 1 + + def test_initialization_without_optional_fields(self): + """Test ValidationResult initialization without optional fields.""" + result = ValidationResult(is_valid=False, syntax_errors=[]) + assert result.is_valid is False + assert result.syntax_errors == [] + assert result.missing_required_imports == [] + + def test_post_init_initializes_missing_imports(self): + """Test that __post_init__ initializes missing_required_imports to empty list.""" + result = ValidationResult(is_valid=True, syntax_errors=[], missing_required_imports=None) + assert result.missing_required_imports == [] + + +class TestPythonSyntaxValidator: + """Test cases for PythonSyntaxValidator class.""" + + @pytest.fixture + def validator(self): + """Create a validator instance for testing.""" + return PythonSyntaxValidator() + + @pytest.fixture + def cleanup_sys_modules(self): + """Cleanup sys.modules after tests to avoid interference.""" + yield + # Remove any mock modules added during testing + modules_to_remove = [k for k in sys.modules.keys() if "mcpworkbench" in k or "temp_validation" in k] + for module in modules_to_remove: + del sys.modules[module] + + # ============================================================================ + # Test validate_code method + # ============================================================================ + + def test_validate_code_success_with_mcp_tool_import(self, validator, cleanup_sys_modules): + """Test validation succeeds with mcp_tool import.""" + code = """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool(name='test', description='test') +def my_tool(): + return "Hello, World!" +""" + result = validator.validate_code(code) + assert result.is_valid is True + assert len(result.syntax_errors) == 0 + assert len(result.missing_required_imports) == 0 + + def test_validate_code_success_with_base_tool_import(self, validator, cleanup_sys_modules): + """Test validation succeeds with BaseTool import.""" + code = """ +from mcpworkbench.core.base_tool import BaseTool + +class MyTool(BaseTool): + def __init__(self): + super().__init__(name='test', description='test') + + async def execute(self): + return "Hello" +""" + result = validator.validate_code(code) + assert result.is_valid is True + assert len(result.syntax_errors) == 0 + assert len(result.missing_required_imports) == 0 + + def test_validate_code_fails_with_code_too_large(self, validator): + """Test validation fails when code exceeds size limit.""" + code = "x = 1\n" * 100_000 # Exceed 100KB limit + result = validator.validate_code(code) + assert result.is_valid is False + assert len(result.syntax_errors) == 1 + assert result.syntax_errors[0]["type"] == "CodeTooLarge" + assert "exceeds maximum allowed" in result.syntax_errors[0]["message"] + + def test_validate_code_fails_with_empty_code(self, validator): + """Test validation fails with empty code.""" + result = validator.validate_code("") + assert result.is_valid is False + assert len(result.syntax_errors) == 1 + assert result.syntax_errors[0]["type"] == "EmptyCode" + + def test_validate_code_fails_with_whitespace_only(self, validator): + """Test validation fails with whitespace-only code.""" + result = validator.validate_code(" \n\t\n ") + assert result.is_valid is False + assert len(result.syntax_errors) == 1 + assert result.syntax_errors[0]["type"] == "EmptyCode" + + def test_validate_code_fails_with_syntax_error(self, validator): + """Test validation fails with syntax error.""" + code = """ +def broken_function( + print('missing closing parenthesis') +""" + result = validator.validate_code(code) + assert result.is_valid is False + assert len(result.syntax_errors) > 0 + assert result.syntax_errors[0]["type"] == "SyntaxError" + + def test_validate_code_fails_with_missing_required_imports(self, validator, cleanup_sys_modules): + """Test validation fails when missing required MCP imports.""" + code = """ +def my_function(): + return "No MCP imports" +""" + result = validator.validate_code(code) + assert result.is_valid is False + assert len(result.missing_required_imports) > 0 + assert "required MCP Workbench imports" in result.missing_required_imports[0] + + def test_validate_code_with_star_import(self, validator, cleanup_sys_modules): + """Test validation succeeds with star import.""" + code = """ +from mcpworkbench.core.annotations import * + +@mcp_tool(name='test', description='test') +def my_tool(): + return "Hello" +""" + result = validator.validate_code(code) + assert result.is_valid is True + + def test_validate_code_with_import_error(self, validator, cleanup_sys_modules): + """Test validation catches import errors.""" + code = """ +from mcpworkbench.core.annotations import mcp_tool +import nonexistent_module + +@mcp_tool(name='test', description='test') +def my_tool(): + return nonexistent_module.something() +""" + result = validator.validate_code(code) + assert result.is_valid is False + assert any(error["type"] == "ImportError" for error in result.syntax_errors) + + def test_validate_code_with_name_error(self, validator, cleanup_sys_modules): + """Test validation catches name errors when they occur at module level.""" + code = """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool(name='test', description='test') +def my_tool(): + return "ok" + +# This will cause a NameError at module level +result = undefined_variable +""" + result = validator.validate_code(code) + assert result.is_valid is False + assert any(error["type"] == "NameError" for error in result.syntax_errors) + + def test_validate_code_with_builtins(self, validator, cleanup_sys_modules): + """Test validation succeeds with Python built-ins.""" + code = """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool(name='test', description='test') +def my_tool(): + data = [1, 2, 3] + return len(data) + sum(data) +""" + result = validator.validate_code(code) + assert result.is_valid is True + + @patch("ast.parse") + def test_validate_code_handles_parse_exception(self, mock_parse, validator): + """Test validation handles non-SyntaxError parse exceptions.""" + mock_parse.side_effect = ValueError("Unexpected parse error") + code = "def test(): pass" + result = validator.validate_code(code) + assert result.is_valid is False + assert len(result.syntax_errors) == 1 + assert result.syntax_errors[0]["type"] == "ParseError" + assert "Failed to parse code" in result.syntax_errors[0]["message"] + + # ============================================================================ + # Test _validate_module_execution method + # ============================================================================ + + def test_validate_module_execution_success(self, validator, cleanup_sys_modules): + """Test successful module execution validation.""" + code = """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool(name='test', description='test') +def my_tool(): + return "Success" +""" + errors = validator._validate_module_execution(code) + assert len(errors) == 0 + + def test_validate_module_execution_with_syntax_error_in_exec(self, validator, cleanup_sys_modules): + """Test module execution catches syntax errors during exec.""" + # This is a corner case where AST might pass but exec fails + code = """ +from mcpworkbench.core.annotations import mcp_tool +# This will pass AST but might fail in exec in some edge cases +""" + # We expect this to pass, but we're testing the error handling path + errors = validator._validate_module_execution(code) + # This should succeed, but we're verifying the method works + assert isinstance(errors, list) + + @patch("importlib.util.spec_from_loader") + def test_validate_module_execution_spec_creation_fails(self, mock_spec, validator): + """Test module execution handles spec creation failure.""" + mock_spec.return_value = None + code = "def test(): pass" + errors = validator._validate_module_execution(code) + assert len(errors) == 1 + assert errors[0]["type"] == "ModuleError" + assert "Failed to create module spec" in errors[0]["message"] + + def test_validate_module_execution_with_generic_exception(self, validator, cleanup_sys_modules): + """Test module execution handles generic exceptions.""" + code = """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool(name='test', description='test') +def my_tool(): + return "ok" + +# Trigger an error at module level +raise RuntimeError("Test error") +""" + errors = validator._validate_module_execution(code) + assert len(errors) > 0 + assert any(error["type"] == "ExecutionError" for error in errors) + + # ============================================================================ + # Test _setup_mcp_environment method + # ============================================================================ + + def test_setup_mcp_environment_creates_mocks(self, validator, cleanup_sys_modules): + """Test MCP environment setup creates mock modules.""" + # Ensure real mcpworkbench is not in sys.modules + modules_to_remove = [k for k in sys.modules.keys() if "mcpworkbench" in k] + for module in modules_to_remove: + del sys.modules[module] + + validator._setup_mcp_environment(None) + + assert "mcpworkbench" in sys.modules + assert "mcpworkbench.core" in sys.modules + assert "mcpworkbench.core.base_tool" in sys.modules + assert "mcpworkbench.core.annotations" in sys.modules + + def test_setup_mcp_environment_uses_existing_modules(self, validator, cleanup_sys_modules): + """Test MCP environment setup uses existing modules if available.""" + # Pre-populate sys.modules with a mock mcpworkbench + mock_module = ModuleType("mcpworkbench.core.base_tool") + sys.modules["mcpworkbench.core.base_tool"] = mock_module + + validator._setup_mcp_environment(None) + + # Should not replace existing module + assert sys.modules["mcpworkbench.core.base_tool"] is mock_module + + def test_setup_mcp_environment_logs_info(self, validator, cleanup_sys_modules): + """Test MCP environment setup logs appropriate info messages.""" + # Remove mcpworkbench from sys.modules to trigger mock setup + modules_to_remove = [k for k in sys.modules.keys() if "mcpworkbench" in k] + for module in modules_to_remove: + del sys.modules[module] + + with patch("mcp_workbench.syntax_validator.logger") as mock_logger: + validator._setup_mcp_environment(None) + + # Should have logged info about setting up mocks + assert mock_logger.info.called + # Check that it logged about finding or not finding real MCP Workbench + call_args_list = [str(call) for call in mock_logger.info.call_args_list] + assert any("MCP" in str(call) for call in call_args_list) + + # ============================================================================ + # Test _check_required_mcp_imports method + # ============================================================================ + + def test_check_required_mcp_imports_with_mcp_tool(self, validator): + """Test required imports check with mcp_tool import.""" + code = "from mcpworkbench.core.annotations import mcp_tool" + tree = ast.parse(code) + missing = validator._check_required_mcp_imports(tree) + assert len(missing) == 0 + + def test_check_required_mcp_imports_with_base_tool(self, validator): + """Test required imports check with BaseTool import.""" + code = "from mcpworkbench.core.base_tool import BaseTool" + tree = ast.parse(code) + missing = validator._check_required_mcp_imports(tree) + assert len(missing) == 0 + + def test_check_required_mcp_imports_with_star_import(self, validator): + """Test required imports check with star import.""" + code = "from mcpworkbench.core.annotations import *" + tree = ast.parse(code) + missing = validator._check_required_mcp_imports(tree) + assert len(missing) == 0 + + def test_check_required_mcp_imports_missing(self, validator): + """Test required imports check when imports are missing.""" + code = "def my_function(): pass" + tree = ast.parse(code) + missing = validator._check_required_mcp_imports(tree) + assert len(missing) > 0 + + def test_check_required_mcp_imports_with_both_imports(self, validator): + """Test required imports check with both mcp_tool and BaseTool.""" + code = """ +from mcpworkbench.core.annotations import mcp_tool +from mcpworkbench.core.base_tool import BaseTool +""" + tree = ast.parse(code) + missing = validator._check_required_mcp_imports(tree) + assert len(missing) == 0 + + # ============================================================================ + # Test _collect_imports method + # ============================================================================ + + def test_collect_imports_direct_import(self, validator): + """Test collecting direct module imports.""" + code = "import os" + tree = ast.parse(code) + imports = validator._collect_imports(tree) + assert "os" in imports["modules"] + + def test_collect_imports_from_import(self, validator): + """Test collecting from imports.""" + code = "from os import path" + tree = ast.parse(code) + imports = validator._collect_imports(tree) + assert "os" in imports["from_imports"] + assert "path" in imports["from_imports"]["os"] + + def test_collect_imports_with_alias(self, validator): + """Test collecting imports with aliases.""" + code = "import numpy as np" + tree = ast.parse(code) + imports = validator._collect_imports(tree) + assert "numpy" in imports["modules"] + assert "np" in imports["aliases"] + assert imports["aliases"]["np"] == "numpy" + + def test_collect_imports_star_import(self, validator): + """Test collecting star imports.""" + code = "from os import *" + tree = ast.parse(code) + imports = validator._collect_imports(tree) + assert "os" in imports["star_imports"] + + def test_collect_imports_multiple_imports(self, validator): + """Test collecting multiple imports in one statement.""" + code = "from os import path, environ, getcwd" + tree = ast.parse(code) + imports = validator._collect_imports(tree) + assert "os" in imports["from_imports"] + assert "path" in imports["from_imports"]["os"] + assert "environ" in imports["from_imports"]["os"] + assert "getcwd" in imports["from_imports"]["os"] + + def test_collect_imports_from_import_with_alias(self, validator): + """Test collecting from imports with aliases.""" + code = "from os import path as p" + tree = ast.parse(code) + imports = validator._collect_imports(tree) + assert "os" in imports["from_imports"] + assert "path" in imports["from_imports"]["os"] + assert "p" in imports["aliases"] + assert imports["aliases"]["p"] == "os.path" + + def test_collect_imports_complex_scenario(self, validator): + """Test collecting various import types together.""" + code = """ +import os +import sys as system +from pathlib import Path +from typing import List, Dict +from collections import * +""" + tree = ast.parse(code) + imports = validator._collect_imports(tree) + + assert "os" in imports["modules"] + assert "sys" in imports["modules"] + assert "system" in imports["aliases"] + assert "pathlib" in imports["from_imports"] + assert "Path" in imports["from_imports"]["pathlib"] + assert "typing" in imports["from_imports"] + assert "List" in imports["from_imports"]["typing"] + assert "Dict" in imports["from_imports"]["typing"] + assert "collections" in imports["star_imports"] + + # ============================================================================ + # Test _format_syntax_error method + # ============================================================================ + + def test_format_syntax_error_with_all_fields(self, validator): + """Test formatting syntax error with all fields present.""" + syntax_error = SyntaxError("invalid syntax") + syntax_error.lineno = 10 + syntax_error.offset = 5 + syntax_error.text = "def test(" + + formatted = validator._format_syntax_error(syntax_error) + + assert formatted["type"] == "SyntaxError" + assert formatted["message"] == "invalid syntax" + assert formatted["line"] == 10 + assert formatted["column"] == 5 + assert formatted["text"] == "def test(" + + def test_format_syntax_error_with_missing_fields(self, validator): + """Test formatting syntax error with missing fields.""" + syntax_error = SyntaxError() + syntax_error.msg = None + syntax_error.lineno = None + syntax_error.offset = None + syntax_error.text = None + + formatted = validator._format_syntax_error(syntax_error) + + assert formatted["type"] == "SyntaxError" + assert formatted["message"] == "Syntax error" + assert formatted["line"] == 0 + assert formatted["column"] == 0 + assert formatted["text"] == "" + + def test_format_syntax_error_with_whitespace_text(self, validator): + """Test formatting syntax error strips whitespace from text.""" + syntax_error = SyntaxError("test error") + syntax_error.lineno = 1 + syntax_error.offset = 1 + syntax_error.text = " def test() \n" + + formatted = validator._format_syntax_error(syntax_error) + + assert formatted["text"] == "def test()" + + # ============================================================================ + # Test initialization + # ============================================================================ + + def test_validator_initialization(self, validator): + """Test validator initializes with correct default values.""" + assert validator.max_code_size == 100_000 + assert isinstance(validator.REQUIRED_MCP_IMPORTS, list) + assert len(validator.REQUIRED_MCP_IMPORTS) == 2 + + def test_required_mcp_imports_structure(self, validator): + """Test REQUIRED_MCP_IMPORTS has correct structure.""" + for module, name in validator.REQUIRED_MCP_IMPORTS: + assert isinstance(module, str) + assert isinstance(name, str) + assert len(module) > 0 + assert len(name) > 0 + + # ============================================================================ + # Test edge cases and integration scenarios + # ============================================================================ + + def test_validate_code_with_comments(self, validator, cleanup_sys_modules): + """Test validation handles code with comments.""" + code = """ +# This is a comment +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool(name='test', description='test') +def my_tool(): + # Another comment + return "Hello" +""" + result = validator.validate_code(code) + assert result.is_valid is True + + def test_validate_code_with_multiline_strings(self, validator, cleanup_sys_modules): + """Test validation handles multiline strings.""" + code = ''' +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool(name='test', description='test') +def my_tool(): + """ + This is a multiline + docstring. + """ + return """ + Multiline + return value + """ +''' + result = validator.validate_code(code) + assert result.is_valid is True + + def test_validate_code_with_nested_functions(self, validator, cleanup_sys_modules): + """Test validation handles nested functions.""" + code = """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool(name='test', description='test') +def outer(): + def inner(): + return "nested" + return inner() +""" + result = validator.validate_code(code) + assert result.is_valid is True + + def test_validate_code_with_class_definition(self, validator, cleanup_sys_modules): + """Test validation handles class definitions.""" + code = """ +from mcpworkbench.core.base_tool import BaseTool + +class MyTool(BaseTool): + def __init__(self): + super().__init__(name='test', description='test') + + async def execute(self): + return self.name +""" + result = validator.validate_code(code) + assert result.is_valid is True + + def test_validate_code_at_size_boundary(self, validator): + """Test validation at exact size boundary.""" + # Create code that's exactly at the limit + code = "x = 1\n" * (validator.max_code_size // 6) # 6 bytes per line + # Ensure it's just under the limit + code = code[: validator.max_code_size - 100] + result = validator.validate_code(code) + # Should fail due to missing imports, but not due to size + assert not any(error["type"] == "CodeTooLarge" for error in result.syntax_errors) + + def test_validate_code_with_encoding_declaration(self, validator, cleanup_sys_modules): + """Test validation handles encoding declarations.""" + code = """# -*- coding: utf-8 -*- +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool(name='test', description='test') +def my_tool(): + return "Hello" +""" + result = validator.validate_code(code) + assert result.is_valid is True + + def test_validate_code_with_future_imports(self, validator, cleanup_sys_modules): + """Test validation handles future imports.""" + code = """ +from __future__ import annotations +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool(name='test', description='test') +def my_tool() -> str: + return "Hello" +""" + result = validator.validate_code(code) + assert result.is_valid is True diff --git a/test_enhanced_validation.py b/test_enhanced_validation.py new file mode 100644 index 000000000..d92d248dc --- /dev/null +++ b/test_enhanced_validation.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# 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. + +"""Test script for the enhanced syntax validation with import checking.""" + +import os +import sys + +from mcp_workbench.syntax_validator import PythonSyntaxValidator + +# Add the lambda directory to the path so we can import the modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "lambda")) + + +def test_validation(): + """Test the enhanced validation functionality.""" + validator = PythonSyntaxValidator() + + print("🧪 Testing Enhanced Python Syntax Validation") + print("=" * 50) + + # Test cases + test_cases = [ + { + "name": "Valid MCP Tool with required import", + "code": """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool +def my_tool(): + \"\"\"A simple MCP tool.\"\"\" + return "Hello, World!" +""", + "should_be_valid": True, + }, + { + "name": "Valid MCP Tool with BaseTool import", + "code": """ +from mcpworkbench.core.base_tool import BaseTool + +class MyTool(BaseTool): + \"\"\"A tool that extends BaseTool.\"\"\" + + def execute(self): + return "Hello from BaseTool!" +""", + "should_be_valid": True, + }, + { + "name": "Missing required MCP imports", + "code": """ +def my_function(): + \"\"\"A function without MCP imports.\"\"\" + return "This should fail validation" +""", + "should_be_valid": False, + }, + { + "name": "Missing general imports", + "code": """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool +def my_tool(): + \"\"\"Uses requests without importing it.\"\"\" + response = requests.get("https://api.example.com") + return response.json() +""", + "should_be_valid": False, + }, + { + "name": "Valid code with proper imports", + "code": """ +from mcpworkbench.core.annotations import mcp_tool +import requests +import json + +@mcp_tool +def fetch_data(): + \"\"\"Fetches data from an API.\"\"\" + response = requests.get("https://api.example.com") + return json.loads(response.text) +""", + "should_be_valid": True, + }, + { + "name": "Syntax error", + "code": """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool +def broken_function( + \"\"\"Missing closing parenthesis.\"\"\" + return "This has a syntax error" +""", + "should_be_valid": False, + }, + { + "name": "Code with built-ins (should be valid)", + "code": """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool +def use_builtins(): + \"\"\"Uses Python built-ins.\"\"\" + data = [1, 2, 3, 4, 5] + return len(data) + sum(data) +""", + "should_be_valid": True, + }, + ] + + # Run tests + passed = 0 + failed = 0 + + for i, test_case in enumerate(test_cases, 1): + print(f"\n🔍 Test {i}: {test_case['name']}") + print("-" * 40) + + try: + result = validator.validate_code(test_case["code"]) + + # Check if the result matches expectation + if result.is_valid == test_case["should_be_valid"]: + print(f"✅ PASSED - Valid: {result.is_valid}") + passed += 1 + else: + print(f"❌ FAILED - Expected: {test_case['should_be_valid']}, Got: {result.is_valid}") + failed += 1 + + # Print detailed results + print(f" Syntax Errors: {len(result.syntax_errors)}") + print(f" Missing Required Imports: {len(result.missing_required_imports)}") + + # Print specific issues if any + if result.syntax_errors: + print(" 📋 Syntax Errors:") + for error in result.syntax_errors: + print(f" - {error['type']}: {error['message']}") + + if result.missing_required_imports: + print(" 📋 Missing Required Imports:") + for missing in result.missing_required_imports: + print(f" - {missing}") + + except Exception as e: + print(f"❌ ERROR - Exception occurred: {e}") + failed += 1 + + # Summary + print("\n" + "=" * 50) + print(f"📊 Test Summary: {passed} passed, {failed} failed") + + if failed == 0: + print("🎉 All tests passed!") + return True + else: + print("⚠️ Some tests failed.") + return False + + +if __name__ == "__main__": + success = test_validation() + sys.exit(0 if success else 1) diff --git a/test_simplified_validation.py b/test_simplified_validation.py new file mode 100644 index 000000000..161d1be73 --- /dev/null +++ b/test_simplified_validation.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +# 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 os +import sys + +from mcp_workbench.syntax_validator import PythonSyntaxValidator + +# Add the lambda directory to the Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "lambda")) + + +def test_simplified_validator(): + """Test the simplified syntax validator.""" + validator = PythonSyntaxValidator() + + # Test 1: Valid code with required imports + valid_code = """ +from mcpworkbench.core.annotations import mcp_tool +from mcpworkbench.core.base_tool import BaseTool + +@mcp_tool(name='test_tool', description='A test tool') +def test_function(): + return 'Hello World' + +class TestTool(BaseTool): + def __init__(self): + super().__init__(name='test', description='Test tool') +""" + + print("=== Test 1: Valid code with required imports ===") + result = validator.validate_code(valid_code) + print(f"Valid: {result.is_valid}") + print(f"Syntax errors: {len(result.syntax_errors)}") + print(f"Missing imports: {len(result.missing_required_imports)}") + if result.syntax_errors: + for error in result.syntax_errors: + print(f' Error: {error["type"]} - {error["message"]}') + + # Test 2: Syntax error + syntax_error_code = """ +def broken_function( + print('missing closing parenthesis') +""" + + print("\n=== Test 2: Syntax error ===") + result = validator.validate_code(syntax_error_code) + print(f"Valid: {result.is_valid}") + print(f"Syntax errors: {len(result.syntax_errors)}") + if result.syntax_errors: + print(f'Error type: {result.syntax_errors[0]["type"]}') + print(f'Error message: {result.syntax_errors[0]["message"]}') + + # Test 3: Missing required imports + missing_imports_code = """ +def some_function(): + return 'No MCP imports here' +""" + + print("\n=== Test 3: Missing required imports ===") + result = validator.validate_code(missing_imports_code) + print(f"Valid: {result.is_valid}") + print(f"Syntax errors: {len(result.syntax_errors)}") + print(f"Missing imports: {len(result.missing_required_imports)}") + if result.missing_required_imports: + print(f"Missing import message: {result.missing_required_imports[0]}") + + # Test 4: Import error (undefined module) + import_error_code = """ +from mcpworkbench.core.annotations import mcp_tool +import nonexistent_module + +def test_function(): + return nonexistent_module.something() +""" + + print("\n=== Test 4: Import error ===") + result = validator.validate_code(import_error_code) + print(f"Valid: {result.is_valid}") + print(f"Syntax errors: {len(result.syntax_errors)}") + if result.syntax_errors: + for error in result.syntax_errors: + print(f' Error: {error["type"]} - {error["message"]}') + + print("\n=== All tests completed ===") + + +if __name__ == "__main__": + test_simplified_validator() From f0797ac0019acce14560f56da4afe3ac0ee7473d Mon Sep 17 00:00:00 2001 From: bedanley Date: Tue, 28 Oct 2025 09:04:18 -0600 Subject: [PATCH 10/21] Feature/mcp stack tests --- .github/workflows/code.release.branch.yml | 6 + .github/workflows/test-and-lint.yml | 3 + .pre-commit-config.yaml | 2 +- Makefile | 69 +- lib/api-base/ecsCluster.ts | 192 +- lib/api-base/fastApiContainer.ts | 102 +- lib/core/iam/ecs.json | 9 + lib/serve/index.ts | 7 - lib/serve/mcpWorkbenchConstruct.ts | 4 + lib/serve/mcpWorkbenchServiceConstruct.ts | 160 + lib/serve/mcpWorkbenchStack.ts | 244 +- lib/serve/serveApplicationConstruct.ts | 230 +- lib/stages.ts | 5 +- scripts/generate-baseline.sh | 34 + test/cdk/mocks/ConfigParser.ts | 3 +- test/cdk/mocks/MockApp.ts | 9 +- test/cdk/mocks/assets.yaml | 26 + test/cdk/mocks/config-test.yaml | 44 +- test/cdk/stacks/README.md | 47 + .../cdk/stacks/__baselines__/LisaApiBase.json | 507 ++ .../__baselines__/LisaApiDeployment.json | 103 + test/cdk/stacks/__baselines__/LisaChat.json | 5654 +++++++++++++++ test/cdk/stacks/__baselines__/LisaCore.json | 305 + test/cdk/stacks/__baselines__/LisaDocs.json | 811 +++ test/cdk/stacks/__baselines__/LisaIAM.json | 808 +++ .../__baselines__/LisaMcpWorkbench.json | 1744 +++++ .../cdk/stacks/__baselines__/LisaMetrics.json | 935 +++ test/cdk/stacks/__baselines__/LisaModels.json | 4097 +++++++++++ .../stacks/__baselines__/LisaNetworking.json | 717 ++ test/cdk/stacks/__baselines__/LisaRAG.json | 6360 +++++++++++++++++ test/cdk/stacks/__baselines__/LisaServe.json | 2959 ++++++++ test/cdk/stacks/__baselines__/LisaUI.json | 748 ++ test/cdk/stacks/overrideConfig.test.ts | 86 + test/cdk/stacks/snapshot.test.ts | 68 + 34 files changed, 26506 insertions(+), 592 deletions(-) create mode 100644 lib/serve/mcpWorkbenchServiceConstruct.ts create mode 100755 scripts/generate-baseline.sh create mode 100644 test/cdk/mocks/assets.yaml create mode 100644 test/cdk/stacks/README.md create mode 100644 test/cdk/stacks/__baselines__/LisaApiBase.json create mode 100644 test/cdk/stacks/__baselines__/LisaApiDeployment.json create mode 100644 test/cdk/stacks/__baselines__/LisaChat.json create mode 100644 test/cdk/stacks/__baselines__/LisaCore.json create mode 100644 test/cdk/stacks/__baselines__/LisaDocs.json create mode 100644 test/cdk/stacks/__baselines__/LisaIAM.json create mode 100644 test/cdk/stacks/__baselines__/LisaMcpWorkbench.json create mode 100644 test/cdk/stacks/__baselines__/LisaMetrics.json create mode 100644 test/cdk/stacks/__baselines__/LisaModels.json create mode 100644 test/cdk/stacks/__baselines__/LisaNetworking.json create mode 100644 test/cdk/stacks/__baselines__/LisaRAG.json create mode 100644 test/cdk/stacks/__baselines__/LisaServe.json create mode 100644 test/cdk/stacks/__baselines__/LisaUI.json create mode 100644 test/cdk/stacks/overrideConfig.test.ts create mode 100644 test/cdk/stacks/snapshot.test.ts diff --git a/.github/workflows/code.release.branch.yml b/.github/workflows/code.release.branch.yml index c5cb3c4aa..6683dbbc7 100644 --- a/.github/workflows/code.release.branch.yml +++ b/.github/workflows/code.release.branch.yml @@ -58,6 +58,12 @@ jobs: echo ${RELEASE_TAG:1} > VERSION # Update package-lock.json to reflect new version npm install + # Regenerate CDK baselines from MockApp + echo "📝 Regenerating CDK baselines..." + rm -rf test/cdk/stacks/__baselines__ + mkdir -p test/cdk/stacks/__baselines__ + npm test -- test/cdk/stacks/snapshot.test.ts --testNamePattern="is compatible with baseline" + echo "✅ CDK baselines regenerated" # Add the generated PR description to the top of CHANGELOG.md echo "📝 Adding release notes to CHANGELOG.md..." # Create a temporary file with the new content diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml index 42107f79e..a2c57c880 100644 --- a/.github/workflows/test-and-lint.yml +++ b/.github/workflows/test-and-lint.yml @@ -75,6 +75,9 @@ jobs: fi pip install -e ./lisa-sdk - name: Run tests + env: + ACCOUNT_NUMBER: '012345678901' + REGION: us-east-1 run: | make test-coverage pre-commit: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f68be4d2d..41ab94870 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: hooks: - id: detect-secrets exclude: (?x)^( - .*.ipynb|config.yaml|.*.md|.*test.*.py + .*.ipynb|config.yaml|.*.md|.*test.*.py|test/cdk/stacks/__baselines__/.*\.json )$ - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/Makefile b/Makefile index ed8cd4257..f3d6afeef 100644 --- a/Makefile +++ b/Makefile @@ -13,33 +13,30 @@ PROJECT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) HEADLESS = false DOCKER_CMD ?= $(or $(CDK_DOCKER),docker) +# Function to read config with fallback to base config and default value +# Usage: VAR := $(call get_config,property,default_value) +define get_config +$(shell test -f $(PROJECT_DIR)/config-custom.yaml && yq $(1) $(PROJECT_DIR)/config-custom.yaml 2>/dev/null | grep -v '^null$$' || \ + (test -f $(PROJECT_DIR)/config-base.yaml && yq $(1) $(PROJECT_DIR)/config-base.yaml 2>/dev/null | grep -v '^null$$') || \ + echo "$(2)") +endef # PROFILE (optional argument) ifeq (${PROFILE},) -TEMP_PROFILE := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .profile) -ifneq ($(TEMP_PROFILE), null) -PROFILE := ${TEMP_PROFILE} -else -$(warning profile is not set in the command line using PROFILE variable or config files, attempting deployment without this variable) +PROFILE := $(call get_config,.profile,) +ifeq ($(PROFILE),) +$(warning profile is not set in command line using PROFILE variable or config files, attempting deployment without this variable) endif endif # DEPLOYMENT_NAME ifeq (${DEPLOYMENT_NAME},) -DEPLOYMENT_NAME := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .deploymentName) -endif - -ifeq (${DEPLOYMENT_NAME}, null) -DEPLOYMENT_NAME := $(shell cat $(PROJECT_DIR)/config-base.yaml | yq .deploymentName) -endif - -ifeq (${DEPLOYMENT_NAME}, null) -DEPLOYMENT_NAME := prod +DEPLOYMENT_NAME := $(call get_config,.deploymentName,prod) endif # ACCOUNT_NUMBER ifeq (${ACCOUNT_NUMBER},) -ACCOUNT_NUMBER := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .accountNumber) +ACCOUNT_NUMBER := $(call get_config,.accountNumber,) endif ifeq (${ACCOUNT_NUMBER},) @@ -48,18 +45,16 @@ endif # REGION ifeq (${REGION},) -REGION := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .region) +REGION := $(call get_config,.region,) endif ifeq (${REGION},) $(error region must be set in command line using REGION variable or config files) endif +# PARTITION ifeq (${PARTITION},) -PARTITION := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .partition ) -endif -ifeq (${PARTITION}, null) -PARTITION := aws +PARTITION := $(call get_config,.partition,aws) endif # DOMAIN - used for the docker login @@ -76,29 +71,13 @@ endif # Arguments defined through config files # APP_NAME -APP_NAME := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .appName) -ifeq (${APP_NAME}, null) -APP_NAME := $(shell cat $(PROJECT_DIR)/config-base.yaml | yq .appName) -endif - -ifeq (${APP_NAME}, null) -APP_NAME := lisa -endif +APP_NAME := $(call get_config,.appName,lisa) # DEPLOYMENT_STAGE -DEPLOYMENT_STAGE := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .deploymentStage) -ifeq (${DEPLOYMENT_STAGE}, null) -DEPLOYMENT_STAGE := $(shell cat $(PROJECT_DIR)/config-base.yaml | yq .deploymentStage) -endif - -ifeq (${DEPLOYMENT_STAGE}, null) -DEPLOYMENT_STAGE := prod -endif +DEPLOYMENT_STAGE := $(call get_config,.deploymentStage,prod) # ACCOUNT_NUMBERS_ECR - AWS account numbers that need to be logged into with Docker CLI to use ECR -ifneq ($(shell yq '.accountNumbersEcr' $(PROJECT_DIR)/config-custom.yaml), null) -ACCOUNT_NUMBERS_ECR := $(shell yq '.accountNumbersEcr[]' $(PROJECT_DIR)/config-custom.yaml) -endif +ACCOUNT_NUMBERS_ECR := $(shell test -f $(PROJECT_DIR)/config-custom.yaml && yq '.accountNumbersEcr[]' $(PROJECT_DIR)/config-custom.yaml 2>/dev/null || echo "") # Append deployed account number to array for dockerLogin rule ACCOUNT_NUMBERS_ECR := $(ACCOUNT_NUMBERS_ECR) $(ACCOUNT_NUMBER) @@ -113,19 +92,13 @@ ifneq ($(findstring $(DEPLOYMENT_STAGE),$(STACK)),$(DEPLOYMENT_STAGE)) endif # MODEL_IDS - IDs of models to deploy -ifneq ($(shell cat $(PROJECT_DIR)/config-custom.yaml | yq '.ecsModels'), null) -MODEL_IDS := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq '.ecsModels[].modelName') -endif +MODEL_IDS := $(shell test -f $(PROJECT_DIR)/config-custom.yaml && yq '.ecsModels[].modelName' $(PROJECT_DIR)/config-custom.yaml 2>/dev/null || echo "") # MODEL_BUCKET - S3 bucket containing model artifacts -MODEL_BUCKET := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq '.s3BucketModels') +MODEL_BUCKET := $(call get_config,.s3BucketModels,) # BASE_URL - Base URL for web UI assets based on domain name and deployment stage -DOMAIN_NAME := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq '.apiGatewayConfig.domainName') -ifeq ($(DOMAIN_NAME), null) -DOMAIN_NAME := $(shell cat $(PROJECT_DIR)/config-base.yaml | yq '.apiGatewayConfig.domainName') -endif - +DOMAIN_NAME := $(call get_config,.apiGatewayConfig.domainName,) ifeq ($(DOMAIN_NAME), null) BASE_URL := /$(DEPLOYMENT_STAGE)/ else diff --git a/lib/api-base/ecsCluster.ts b/lib/api-base/ecsCluster.ts index 193274a08..41cac8c2b 100644 --- a/lib/api-base/ecsCluster.ts +++ b/lib/api-base/ecsCluster.ts @@ -71,6 +71,7 @@ type ECSClusterProps = { ecsConfig: ECSConfig; securityGroup: ISecurityGroup; vpc: Vpc; + environment: Record; } & BaseProps; /** @@ -100,6 +101,16 @@ export class ECSCluster extends Construct { public readonly cluster: Cluster; private readonly targetGroups: Partial> = {}; + private readonly config: Config; + private readonly ecsConfig: ECSConfig; + private readonly vpc: Vpc; + private readonly securityGroup: ISecurityGroup; + private readonly logGroup: LogGroup; + private readonly volumes: Volume[]; + private readonly mountPoints: MountPoint[]; + private readonly baseEnvironment: Record; + private readonly autoScalingGroup: AutoScalingGroup; + private readonly asgCapacityProvider: AsgCapacityProvider; /** * Creates a task definition with its associated container and IAM role (base method). @@ -117,7 +128,6 @@ export class ECSCluster extends Construct { taskDefinitionName: string, config: Config, taskDefinition: TaskDefinition, - baseEnvironment: Record, ecsConfig: ECSConfig, volumes: Volume[], mountPoints: MountPoint[], @@ -159,7 +169,7 @@ export class ECSCluster extends Construct { const container = ec2TaskDefinition.addContainer(createCdkId([taskDefinitionName, 'Container']), { containerName: createCdkId([config.deploymentName, taskDefinitionName], 32, 2), image, - environment: {...baseEnvironment, ...taskDefinition.environment}, + environment: {...this.baseEnvironment, ...taskDefinition.environment}, logging: LogDriver.awsLogs({ logGroup: logGroup, streamPrefix: taskDefinitionName @@ -185,7 +195,7 @@ export class ECSCluster extends Construct { */ constructor (scope: Construct, id: string, props: ECSClusterProps) { super(scope, id); - const { config, identifier, vpc, securityGroup, ecsConfig } = props; + const { config, identifier, vpc, securityGroup, ecsConfig, environment } = props; // Create ECS cluster const cluster = new Cluster(this, createCdkId([config.deploymentName, config.deploymentStage, 'Cl']), { @@ -274,7 +284,7 @@ export class ECSCluster extends Construct { const baseEnvironment: { [key: string]: string; - } = {}; + } = {...environment}; const volumes: Volume[] = []; const mountPoints: MountPoint[] = []; @@ -398,60 +408,94 @@ export class ECSCluster extends Construct { .concat('*') .join(','); - Object.entries(ecsConfig.tasks).forEach(([name, definition]) => { - // Retrieve task role and execution role for each task - const taskRole = Role.fromRoleArn( - this, - createCdkId([name, 'TR']), - StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/${name}`), - ); - const executionRole = Role.fromRoleArn( - this, - createCdkId([name, 'ER']), - StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/${name}EX`), - ); - - const taskResult = this.createTaskDefinition( - name, - config, - definition, - baseEnvironment, - ecsConfig, - volumes, - mountPoints, - logGroup, - taskRole, - executionRole - ); - const { taskDefinition, container } = taskResult; - - // Create ECS service for primary task - const serviceProps: Ec2ServiceProps = { - cluster: cluster, - serviceName: createCdkId([name], 32, 2), - taskDefinition: taskDefinition, - circuitBreaker: !config.region.includes('iso') ? { rollback: true } : undefined, - capacityProviderStrategies: [ - { capacityProvider: asgCapacityProvider.capacityProviderName, weight: 1 } - ] - }; + // Store configuration for later use by addTask method + this.config = config; + this.ecsConfig = ecsConfig; + this.vpc = vpc; + this.securityGroup = securityGroup; + this.logGroup = logGroup; + this.volumes = volumes; + this.mountPoints = mountPoints; + this.baseEnvironment = baseEnvironment; + this.autoScalingGroup = autoScalingGroup; + this.asgCapacityProvider = asgCapacityProvider; + } - const service = new Ec2Service(this, createCdkId([config.deploymentName, name, 'Ec2Svc']), serviceProps); - const scalableTaskCount = service.autoScaleTaskCount({ - minCapacity: 1, - // 10 is just a magic number we don't expect to hit because we don't have better data on this - maxCapacity: 10 - }); - service.node.addDependency(autoScalingGroup); + /** + * Add a task to the ECS cluster with its own target group and service. + * + * @param taskName - The name of the task (e.g., ECSTasks.REST, ECSTasks.MCPWORKBENCH) + * @param taskDefinition - The task definition configuration. Environment variables within task definition will be merged with + * cluster environment variables. + * @param identifier - The identifier for naming resources + * @returns Object containing the created service and target group + */ + public addTask ( + taskName: ECSTasks, + taskDefinition: TaskDefinition, + identifier: string + ): { service: Ec2Service; targetGroup?: ApplicationTargetGroup } { + // Retrieve task role and execution role for the task + const taskRole = Role.fromRoleArn( + this, + createCdkId([taskName, 'TR']), + StringParameter.valueForStringParameter(this, `${this.config.deploymentPrefix}/roles/${taskName}`), + ); + const executionRole = Role.fromRoleArn( + this, + createCdkId([taskName, 'ER']), + StringParameter.valueForStringParameter(this, `${this.config.deploymentPrefix}/roles/${taskName}EX`), + ); - // since our containers are using ephemeral ports, the load balancer must be allowed to access them - service.connections.allowFrom(loadBalancer, Port.allTcp()); + const taskResult = this.createTaskDefinition( + taskName, + this.config, + taskDefinition, + this.ecsConfig, + this.volumes, + this.mountPoints, + this.logGroup, + taskRole, + executionRole + ); + const { taskDefinition: ec2TaskDefinition, container } = taskResult; + + // Store references + this.containers[taskName] = container; + this.taskRoles[taskName] = taskRole; + + // Create ECS service + const serviceProps: Ec2ServiceProps = { + cluster: this.cluster, + serviceName: createCdkId([taskName], 32, 2), + taskDefinition: ec2TaskDefinition, + circuitBreaker: !this.config.region.includes('iso') ? { rollback: true } : undefined, + capacityProviderStrategies: [ + { capacityProvider: this.asgCapacityProvider.capacityProviderName, weight: 1 } + ] + }; + + const service = new Ec2Service(this, createCdkId([this.config.deploymentName, taskName, 'Ec2Svc']), serviceProps); + const scalableTaskCount = service.autoScaleTaskCount({ + minCapacity: 1, + maxCapacity: 10 + }); + service.node.addDependency(this.autoScalingGroup); + + // Store service reference + this.services[taskName] = service; + + // Allow load balancer to access the service + service.connections.allowFrom(this.loadBalancer, Port.allTcp()); - // Create target groups for both services - const loadBalancerHealthCheckConfig = ecsConfig.loadBalancerConfig.healthCheckConfig; + let targetGroup: ApplicationTargetGroup | undefined; - const targetGroup = listener.addTargets(createCdkId([identifier, name, 'TgtGrp']), { - targetGroupName: createCdkId([config.deploymentName, identifier, name], 32, 2).toLowerCase(), + // Only create target group if the task has application target configuration + if (taskDefinition.applicationTarget) { + const loadBalancerHealthCheckConfig = this.ecsConfig.loadBalancerConfig.healthCheckConfig; + + targetGroup = this.listener.addTargets(createCdkId([identifier, taskName, 'TgtGrp']), { + targetGroupName: createCdkId([this.config.deploymentName, identifier, taskName], 32, 2).toLowerCase(), healthCheck: { path: loadBalancerHealthCheckConfig.path, interval: Duration.seconds(loadBalancerHealthCheckConfig.interval), @@ -461,28 +505,30 @@ export class ECSCluster extends Construct { }, port: 80, targets: [service], - priority: definition.applicationTarget?.priority, - conditions: definition.applicationTarget?.conditions?.map(({ type, values }) => { - switch (type) { - case 'pathPatterns': - return ListenerCondition.pathPatterns(values); - } + ...(taskDefinition.applicationTarget.priority && { + priority: taskDefinition.applicationTarget.priority, + conditions: taskDefinition.applicationTarget.conditions?.map(({ type, values }) => { + switch (type) { + case 'pathPatterns': + return ListenerCondition.pathPatterns(values); + } + }) }) }); - scalableTaskCount.scaleOnRequestCount(createCdkId([identifier, 'ScalingPolicy']), { - requestsPerTarget: ecsConfig.autoScalingConfig.metricConfig.targetValue, - targetGroup, - scaleInCooldown: Duration.seconds(ecsConfig.autoScalingConfig.metricConfig.duration), - scaleOutCooldown: Duration.seconds(ecsConfig.autoScalingConfig.metricConfig.duration) - }); + // Store target group reference + this.targetGroups[taskName] = targetGroup; + + if (targetGroup) { + scalableTaskCount.scaleOnRequestCount(createCdkId([identifier, taskName, 'ScalingPolicy']), { + requestsPerTarget: this.ecsConfig.autoScalingConfig.metricConfig.targetValue, + targetGroup, + scaleInCooldown: Duration.seconds(this.ecsConfig.autoScalingConfig.metricConfig.duration), + scaleOutCooldown: Duration.seconds(this.ecsConfig.autoScalingConfig.metricConfig.duration) + }); + } + } - // Store in maps for future reference - const ecsTasksKey = name as keyof typeof ECSTasks; - this.containers[ecsTasksKey] = container; - this.taskRoles[ecsTasksKey] = taskRole; - this.services[ecsTasksKey] = service; - this.targetGroups[ecsTasksKey] = targetGroup; - }); + return { service, targetGroup }; } } diff --git a/lib/api-base/fastApiContainer.ts b/lib/api-base/fastApiContainer.ts index c5b9e6543..c599d0371 100644 --- a/lib/api-base/fastApiContainer.ts +++ b/lib/api-base/fastApiContainer.ts @@ -17,8 +17,7 @@ import { CfnOutput } from 'aws-cdk-lib'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; -import { AmiHardwareType, ContainerDefinition } from 'aws-cdk-lib/aws-ecs'; -import { IRole } from 'aws-cdk-lib/aws-iam'; +import { AmiHardwareType } from 'aws-cdk-lib/aws-ecs'; import { Construct } from 'constructs'; import { dump as yamlDump } from 'js-yaml'; @@ -49,30 +48,19 @@ type FastApiContainerProps = { securityGroup: ISecurityGroup; tokenTable: ITable | undefined; vpc: Vpc; + managementKeyName: string; } & BaseProps; /** * FastApiContainer Construct. */ export class FastApiContainer extends Construct { - /** Map of all container definitions by identifier */ - public readonly containers: ContainerDefinition[] = []; - - /** Map of all task roles by identifier */ - public readonly taskRoles: Partial> = {}; + /** ECS Cluster **/ + public readonly apiCluster: ECSCluster; /** FastAPI URL **/ public readonly endpoint: string; - /** ECS Cluster **/ - public readonly ecsCluster: any; - - /** Application Load Balancer **/ - public readonly loadBalancer: any; - - /** Application Listener **/ - public readonly listener: any; - /** * @param {Construct} scope - The parent or owner of the construct. * @param {string} id - The unique identifier for the construct within its scope. @@ -81,31 +69,43 @@ export class FastApiContainer extends Construct { constructor (scope: Construct, id: string, props: FastApiContainerProps) { super(scope, id); - const { config, securityGroup, tokenTable, vpc } = props; + const { config, securityGroup, tokenTable, vpc, managementKeyName} = props; + + const instanceType = 'm5.large'; + const buildArgs: Record | undefined = { BASE_IMAGE: config.baseImage, PYPI_INDEX_URL: config.pypiConfig.indexUrl, PYPI_TRUSTED_HOST: config.pypiConfig.trustedHost, LITELLM_CONFIG: yamlDump(config.litellmConfig), }; - const baseEnvironment: Record = { + + // Environment variables for all containers + const environment: Record = { LOG_LEVEL: config.logLevel, AWS_REGION: config.region, AWS_REGION_NAME: config.region, // for supporting SageMaker endpoints in LiteLLM - THREADS: Ec2Metadata.get('m5.large').vCpus.toString(), - LITELLM_KEY: config.litellmConfig.db_key, - OPENAI_API_KEY: config.litellmConfig.db_key, - TIKTOKEN_CACHE_DIR: '/app/TIKTOKEN_CACHE', + THREADS: Ec2Metadata.get(instanceType).vCpus.toString(), USE_AUTH: 'true', AUTHORITY: config.authConfig!.authority, CLIENT_ID: config.authConfig!.clientId, ADMIN_GROUP: config.authConfig!.adminGroup, USER_GROUP: config.authConfig!.userGroup, JWT_GROUPS_PROP: config.authConfig!.jwtGroupsProperty, + MANAGEMENT_KEY_NAME: managementKeyName }; if (tokenTable) { - baseEnvironment.TOKEN_TABLE_NAME = tokenTable.tableName; + environment.TOKEN_TABLE_NAME = tokenTable.tableName; + } + + // Requires mount point /etc/pki from host + if (config.region.includes('iso')) { + environment.SSL_CERT_DIR = '/etc/pki/tls/certs'; + environment.SSL_CERT_FILE = config.certificateAuthorityBundle; + environment.REQUESTS_CA_BUNDLE = config.certificateAuthorityBundle; + environment.AWS_CA_BUNDLE = config.certificateAuthorityBundle; + environment.CURL_CA_BUNDLE = config.certificateAuthorityBundle; } // Pre-generate the tiktoken cache to ensure it does not attempt to fetch data from the internet at runtime. @@ -128,7 +128,6 @@ export class FastApiContainer extends Construct { type: EcsSourceType.ASSET }; - const instanceType = 'm5.large'; const healthCheckConfig = { command: ['CMD-SHELL', 'exit 0'], interval: 10, @@ -152,20 +151,7 @@ export class FastApiContainer extends Construct { } }, buildArgs, - tasks: { - [ECSTasks.REST]: { - environment: baseEnvironment, - containerConfig: { - image: restApiImage, - healthCheckConfig, - environment: {}, - sharedMemorySize: 0 - }, - // set a softlimit of what we expect to use - containerMemoryReservationMiB: SERVE_CONTAINER_MEMORY_RESERVATION - }, - - }, + tasks: {}, // reserve at least enough memory for each task and a buffer for the instance to use containerMemoryBuffer: Ec2Metadata.get(instanceType).memory - (INSTANCE_MEMORY_RESERVATION + SERVE_CONTAINER_MEMORY_RESERVATION + (config.deployMcpWorkbench ? WORKBENCH_CONTAINER_MEMORY_RESERVATION : 0)), instanceType, @@ -188,19 +174,38 @@ export class FastApiContainer extends Construct { ecsConfig, config, securityGroup, - vpc + vpc, + environment }); - + // Add the REST API task to the cluster (default target, no priority/conditions) + apiCluster.addTask(ECSTasks.REST, { + environment: { + LITELLM_KEY: config.litellmConfig.db_key, + OPENAI_API_KEY: config.litellmConfig.db_key, + TIKTOKEN_CACHE_DIR: '/app/TIKTOKEN_CACHE', + }, + containerConfig: { + image: restApiImage, + healthCheckConfig, + environment: {}, + sharedMemorySize: 0 + }, + containerMemoryReservationMiB: SERVE_CONTAINER_MEMORY_RESERVATION, + applicationTarget: { + port: 8080 + } + }, props.apiName); if (tokenTable) { - Object.entries(apiCluster.taskRoles).forEach(([, role]) => { - tokenTable.grantReadData(role); - }); + // Grant token table access to REST API task role only + const restTaskRole = apiCluster.taskRoles[ECSTasks.REST]; + if (restTaskRole) { + tokenTable.grantReadData(restTaskRole); + } } - - + this.apiCluster = apiCluster; this.endpoint = apiCluster.endpointUrl; new StringParameter(scope, 'FastApiEndpoint', { @@ -208,13 +213,6 @@ export class FastApiContainer extends Construct { stringValue: this.endpoint }); - // Update - this.containers = Object.values(apiCluster.containers); - this.taskRoles = apiCluster.taskRoles; - this.ecsCluster = apiCluster.cluster; - this.loadBalancer = apiCluster.loadBalancer; - this.listener = apiCluster.listener; - // CFN output new CfnOutput(this, `${props.apiName}Url`, { value: apiCluster.endpointUrl, diff --git a/lib/core/iam/ecs.json b/lib/core/iam/ecs.json index 8d254c369..17710ad00 100644 --- a/lib/core/iam/ecs.json +++ b/lib/core/iam/ecs.json @@ -198,6 +198,15 @@ "arn:${AWS::Partition}:s3:::*-mcpworkbench-*", "arn:${AWS::Partition}:s3:::*-mcpworkbench-*/*" ] + }, + { + "Effect": "Allow", + "Action": [ + "dynamodb:GetItem", + "dynamodb:Query", + "dynamodb:Scan" + ], + "Resource": "arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/*-LISAApiTokenTable" } ] } diff --git a/lib/serve/index.ts b/lib/serve/index.ts index b78de21e4..63d35c797 100644 --- a/lib/serve/index.ts +++ b/lib/serve/index.ts @@ -20,7 +20,6 @@ import { Construct } from 'constructs'; import { FastApiContainer } from '../api-base/fastApiContainer'; import { LisaServeApplicationConstruct, LisaServeApplicationProps } from './serveApplicationConstruct'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; - export * from './serveApplicationConstruct'; /** @@ -32,9 +31,6 @@ export class LisaServeApplicationStack extends Stack { public readonly modelsPs: StringParameter; public readonly endpointUrl: StringParameter; public readonly tokenTable?: ITable; - public readonly ecsCluster: any; - public readonly loadBalancer: any; - public readonly listener: any; /** * @param {Construct} scope - The parent or owner of the construct. @@ -50,8 +46,5 @@ export class LisaServeApplicationStack extends Stack { this.modelsPs = app.modelsPs; this.restApi = app.restApi; this.tokenTable = app.tokenTable; - this.ecsCluster = app.ecsCluster; - this.loadBalancer = app.loadBalancer; - this.listener = app.listener; } } diff --git a/lib/serve/mcpWorkbenchConstruct.ts b/lib/serve/mcpWorkbenchConstruct.ts index 1418b2c33..5459d9f45 100644 --- a/lib/serve/mcpWorkbenchConstruct.ts +++ b/lib/serve/mcpWorkbenchConstruct.ts @@ -37,6 +37,8 @@ export type McpWorkbenchConstructProps = { } & BaseProps & StackProps; export default class McpWorkbenchConstruct extends Construct { + public readonly workbenchBucket: s3.Bucket; + constructor (scope: Construct, id: string, props: McpWorkbenchConstructProps) { super(scope, id); @@ -65,6 +67,8 @@ export default class McpWorkbenchConstruct extends Construct { const lambdaLayers = [commonLambdaLayer, fastapiLambdaLayer]; this.createWorkbenchApi(restApi, rootResourceId, config, vpc, securityGroups, authorizer, workbenchBucket, lambdaLayers); + + this.workbenchBucket = workbenchBucket; } private createWorkbenchApi (restApi: IRestApi, rootResourceId: string, config: Config, vpc: Vpc, securityGroups: ISecurityGroup[], authorizer: IAuthorizer, workbenchBucket: s3.Bucket, lambdaLayers: lambda.ILayerVersion[]) { diff --git a/lib/serve/mcpWorkbenchServiceConstruct.ts b/lib/serve/mcpWorkbenchServiceConstruct.ts new file mode 100644 index 000000000..0a1adad3d --- /dev/null +++ b/lib/serve/mcpWorkbenchServiceConstruct.ts @@ -0,0 +1,160 @@ +/** + 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 { Duration } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { BaseProps, EcsSourceType } from '../schema'; +import { Ec2Service } from 'aws-cdk-lib/aws-ecs'; +import { ECSCluster, ECSTasks } from '../api-base/ecsCluster'; +import { MCP_WORKBENCH_PATH } from '../util'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { LAMBDA_PATH } from '../util'; +import { getDefaultRuntime } from '../api-base/utils'; +import { IBucket } from 'aws-cdk-lib/aws-s3'; + +export type McpWorkbenchServiceConstructProps = { + apiCluster: ECSCluster; + workbenchBucket: IBucket; +} & BaseProps; + +export default class McpWorkbenchServiceConstruct extends Construct { + public readonly service: Ec2Service; + + constructor (scope: Construct, id: string, props: McpWorkbenchServiceConstructProps) { + super(scope, id); + + const { config, apiCluster } = props; + + const mcpWorkbenchImage = config.mcpWorkbenchConfig || { + baseImage: config.baseImage, + path: MCP_WORKBENCH_PATH, + type: EcsSourceType.ASSET + }; + + const mcpWorkbenchTaskDefinition = { + environment: { + RCLONE_CONFIG_S3_REGION: config.region, + MCPWORKBENCH_BUCKET: [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(), + }, + containerConfig: { + image: mcpWorkbenchImage, + healthCheckConfig: { + command: ['CMD-SHELL', 'exit 0'], + interval: 10, + startPeriod: 30, + timeout: 5, + retries: 3 + }, + environment: {}, + sharedMemorySize: 0, + privileged: true + }, + containerMemoryReservationMiB: 1024, + applicationTarget: { + port: 8000, + priority: 80, + conditions: [{ + type: 'pathPatterns' as const, + values: ['/v2/mcp/*'] + }] + } + }; + + const { service } = apiCluster.addTask(ECSTasks.MCPWORKBENCH, mcpWorkbenchTaskDefinition, 'REST'); + this.service = service; + + this.createS3EventHandler(config, service); + } + + private createS3EventHandler (config: any, workbenchService: Ec2Service) { + const s3EventHandlerRole = new Role(this, 'S3EventHandlerRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), + inlinePolicies: { + 'S3EventHandlerPolicy': new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents' + ], + resources: [`arn:${config.partition}:logs:*:*:*`] + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'ecs:UpdateService', + 'ecs:DescribeServices', + 'ecs:DescribeClusters' + ], + resources: [ + `arn:${config.partition}:ecs:${config.region}:*:cluster/${workbenchService.cluster.clusterName}*`, + `arn:${config.partition}:ecs:${config.region}:*:service/${workbenchService.cluster.clusterName}*/${workbenchService.serviceName}*` + ] + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'ssm:GetParameter' + ], + resources: [ + `arn:${config.partition}:ssm:${config.region}:*:parameter${config.deploymentPrefix}/deploymentName` + ] + }) + ] + }) + } + }); + + const s3EventHandlerLambda = new lambda.Function(this, 'S3EventHandlerLambda', { + runtime: getDefaultRuntime(), + handler: 'mcp_workbench.s3_event_handler.handler', + code: lambda.Code.fromAsset(config.lambdaPath ?? LAMBDA_PATH), + timeout: Duration.minutes(2), + role: s3EventHandlerRole, + environment: { + DEPLOYMENT_PREFIX: config.deploymentPrefix!, + API_NAME: 'MCPWorkbench', + ECS_CLUSTER_NAME: workbenchService.cluster.clusterName, + MCPWORKBENCH_SERVICE_NAME: workbenchService.serviceName + } + }); + + const rescanMcpWorkbenchRule = new events.Rule(this, 'RescanMCPWorkbenchRule', { + eventPattern: { + source: ['aws.s3', 'debug'], + detailType: [ + 'Object Created', + 'Object Deleted' + ], + detail: { + bucket: { + name: [[config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase()] + } + } + }, + }); + + rescanMcpWorkbenchRule.addTarget(new targets.LambdaFunction(s3EventHandlerLambda, { + retryAttempts: 2, + maxEventAge: Duration.minutes(5) + })); + } +} diff --git a/lib/serve/mcpWorkbenchStack.ts b/lib/serve/mcpWorkbenchStack.ts index df6386df4..97ff4fdc1 100644 --- a/lib/serve/mcpWorkbenchStack.ts +++ b/lib/serve/mcpWorkbenchStack.ts @@ -14,48 +14,32 @@ limitations under the License. */ -import { Duration, Stack, StackProps } from 'aws-cdk-lib'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; -import { BaseProps, EcsSourceType } from '../schema'; +import { BaseProps } from '../schema'; import McpWorkbenchConstruct from './mcpWorkbenchConstruct'; import { Vpc } from '../networking/vpc'; -import { ICluster, Ec2Service, Ec2TaskDefinition, Protocol, LogDriver, HealthCheck } from 'aws-cdk-lib/aws-ecs'; -import { MCP_WORKBENCH_PATH } from '../util'; -import { dump as yamlDump } from 'js-yaml'; -import * as events from 'aws-cdk-lib/aws-events'; -import * as targets from 'aws-cdk-lib/aws-events-targets'; -import * as lambda from 'aws-cdk-lib/aws-lambda'; -import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; -import { LAMBDA_PATH } from '../util'; -import { getDefaultRuntime } from '../api-base/utils'; -import { createCdkId } from '../core/utils'; -import { CodeFactory } from '../util'; -import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; - -import { StringParameter } from 'aws-cdk-lib/aws-ssm'; -import { Port } from 'aws-cdk-lib/aws-ec2'; -import { ListenerCondition } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import { ECSCluster } from '../api-base/ecsCluster'; +import McpWorkbenchServiceConstruct from './mcpWorkbenchServiceConstruct'; export type McpWorkbenchStackProps = { vpc: Vpc; restApiId: string; rootResourceId: string; authorizerId: string; - ecsCluster: ICluster; - loadBalancer: any; - listener: any; + apiCluster: ECSCluster; } & BaseProps & StackProps; export class McpWorkbenchStack extends Stack { constructor (scope: Construct, id: string, props: McpWorkbenchStackProps) { super(scope, id, props); - const { config, vpc, restApiId, rootResourceId, authorizerId, ecsCluster, loadBalancer, listener } = props; + const { config, vpc, restApiId, rootResourceId, authorizerId, apiCluster } = props; // Import authorizer const authorizer = { authorizerId }; - new McpWorkbenchConstruct(this, 'McpWorkbench', { + const { workbenchBucket } = new McpWorkbenchConstruct(this, 'McpWorkbench', { ...props, authorizer: authorizer as any, restApiId, @@ -63,218 +47,12 @@ export class McpWorkbenchStack extends Stack { securityGroups: [vpc.securityGroups.ecsModelAlbSg], }); - // Create MCP Workbench ECS Service on shared cluster - this.createMcpWorkbenchEcsService(config, vpc, ecsCluster, loadBalancer, listener); - } - - private createMcpWorkbenchEcsService (config: any, vpc: Vpc, ecsCluster: ICluster, loadBalancer: any, listener: any) { - const baseEnvironment: Record = { - LOG_LEVEL: config.logLevel, - AWS_REGION: config.region, - AWS_REGION_NAME: config.region, - LITELLM_KEY: config.litellmConfig.db_key, - OPENAI_API_KEY: config.litellmConfig.db_key, - USE_AUTH: 'true', - AUTHORITY: config.authConfig!.authority, - CLIENT_ID: config.authConfig!.clientId, - ADMIN_GROUP: config.authConfig!.adminGroup, - USER_GROUP: config.authConfig!.userGroup, - JWT_GROUPS_PROP: config.authConfig!.jwtGroupsProperty, - RCLONE_CONFIG_S3_REGION: config.region, - MCPWORKBENCH_BUCKET: [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(), - }; - - const mcpWorkbenchImage = config.mcpWorkbenchConfig || { - baseImage: config.baseImage, - path: MCP_WORKBENCH_PATH, - type: EcsSourceType.ASSET - }; - - const buildArgs: Record = { - BASE_IMAGE: config.baseImage, - PYPI_INDEX_URL: config.pypiConfig.indexUrl, - PYPI_TRUSTED_HOST: config.pypiConfig.trustedHost, - LITELLM_CONFIG: yamlDump(config.litellmConfig), - }; - - // Create CloudWatch log group - const logGroup = new LogGroup(this, createCdkId([config.deploymentPrefix, 'MCPWorkbench', 'LogGroup']), { - logGroupName: `/aws/ecs/${config.deploymentName}-${config.deploymentStage}-MCPWorkbench`, - retention: RetentionDays.ONE_WEEK, - removalPolicy: config.removalPolicy - }); - - // Get task role from parameter store - const taskRole = Role.fromRoleArn( - this, - 'MCPWorkbenchTaskRole', - StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/MCPWORKBENCH`), - ); - const executionRole = Role.fromRoleArn( - this, - 'MCPWorkbenchExecutionRole', - StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/MCPWORKBENCHEX`), - ); - - // Create task definition - const taskDefinition = new Ec2TaskDefinition(this, createCdkId(['MCPWorkbench', 'Ec2TaskDefinition']), { - family: createCdkId([config.deploymentName, 'MCPWorkbench'], 32, 2), - taskRole, - executionRole, - }); - - // Grant CloudWatch logs write permissions - logGroup.grantWrite(taskRole); - logGroup.grantWrite(executionRole); - - const healthCheckConfig = { - command: ['CMD-SHELL', 'exit 0'], - interval: 10, - startPeriod: 30, - timeout: 5, - retries: 3 - }; - - const containerHealthCheck: HealthCheck = { - command: healthCheckConfig.command, - interval: Duration.seconds(healthCheckConfig.interval), - startPeriod: Duration.seconds(healthCheckConfig.startPeriod), - timeout: Duration.seconds(healthCheckConfig.timeout), - retries: healthCheckConfig.retries, - }; - - const image = CodeFactory.createImage(mcpWorkbenchImage, this, 'MCPWorkbench', buildArgs); - - taskDefinition.addContainer(createCdkId(['MCPWorkbench', 'Container']), { - containerName: createCdkId([config.deploymentName, 'MCPWorkbench'], 32, 2), - image, - environment: baseEnvironment, - logging: LogDriver.awsLogs({ - logGroup: logGroup, - streamPrefix: 'MCPWorkbench' - }), - memoryReservationMiB: 1024, - portMappings: [{ hostPort: 0, containerPort: 8000, protocol: Protocol.TCP }], - healthCheck: containerHealthCheck, - privileged: true, + new McpWorkbenchServiceConstruct(this, 'McpWorkbenchService', { + config, + apiCluster, + workbenchBucket, }); - - // Create ECS service - const service = new Ec2Service(this, createCdkId([config.deploymentName, 'MCPWorkbench', 'Ec2Svc']), { - cluster: ecsCluster, - serviceName: createCdkId(['MCPWorkbench'], 32, 2), - taskDefinition: taskDefinition, - circuitBreaker: !config.region.includes('iso') ? { rollback: true } : undefined, - }); - - const scalableTaskCount = service.autoScaleTaskCount({ - minCapacity: 1, - maxCapacity: 10 - }); - - // Connect service to shared load balancer - service.connections.allowFrom(loadBalancer, Port.allTcp()); - - // Create target group for MCP Workbench - const targetGroup = listener.addTargets(createCdkId(['REST', 'MCPWorkbench', 'TgtGrp']), { - targetGroupName: createCdkId([config.deploymentName, 'REST', 'MCPWorkbench'], 32, 2).toLowerCase(), - healthCheck: { - path: '/health', - interval: Duration.seconds(60), - timeout: Duration.seconds(30), - healthyThresholdCount: 2, - unhealthyThresholdCount: 10, - }, - port: 80, - targets: [service], - priority: 80, - conditions: [ListenerCondition.pathPatterns(['/v2/mcp/*'])] - }); - - scalableTaskCount.scaleOnRequestCount(createCdkId(['REST', 'MCPWorkbench', 'ScalingPolicy']), { - requestsPerTarget: 500, - targetGroup, - scaleInCooldown: Duration.seconds(60), - scaleOutCooldown: Duration.seconds(60) - }); - - // Create S3 event handler for MCP Workbench - this.createS3EventHandler(config, service); } - private createS3EventHandler (config: any, workbenchService: Ec2Service) { - const s3EventHandlerRole = new Role(this, 'S3EventHandlerRole', { - assumedBy: new ServicePrincipal('lambda.amazonaws.com'), - inlinePolicies: { - 'S3EventHandlerPolicy': new PolicyDocument({ - statements: [ - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'logs:CreateLogGroup', - 'logs:CreateLogStream', - 'logs:PutLogEvents' - ], - resources: [`arn:${config.partition}:logs:*:*:*`] - }), - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'ecs:UpdateService', - 'ecs:DescribeServices', - 'ecs:DescribeClusters' - ], - resources: [ - `arn:${config.partition}:ecs:${config.region}:*:cluster/${workbenchService.cluster.clusterName}*`, - `arn:${config.partition}:ecs:${config.region}:*:service/${workbenchService.cluster.clusterName}*/${workbenchService.serviceName}*` - ] - }), - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'ssm:GetParameter' - ], - resources: [ - `arn:${config.partition}:ssm:${config.region}:*:parameter${config.deploymentPrefix}/deploymentName` - ] - }) - ] - }) - } - }); - - const s3EventHandlerLambda = new lambda.Function(this, 'S3EventHandlerLambda', { - runtime: getDefaultRuntime(), - handler: 'mcp_workbench.s3_event_handler.handler', - code: lambda.Code.fromAsset(config.lambdaPath ?? LAMBDA_PATH), - timeout: Duration.minutes(2), - role: s3EventHandlerRole, - environment: { - DEPLOYMENT_PREFIX: config.deploymentPrefix!, - API_NAME: 'MCPWorkbench', - ECS_CLUSTER_NAME: workbenchService.cluster.clusterName, - MCPWORKBENCH_SERVICE_NAME: workbenchService.serviceName - } - }); - const rescanMcpWorkbenchRule = new events.Rule(this, 'RescanMCPWorkbenchRule', { - eventPattern: { - source: ['aws.s3', 'debug'], - detailType: [ - 'Object Created', - 'Object Deleted' - ], - detail: { - bucket: { - name: [[config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase()] - } - } - }, - }); - - rescanMcpWorkbenchRule.addTarget(new targets.LambdaFunction(s3EventHandlerLambda, { - retryAttempts: 2, - maxEventAge: Duration.minutes(5) - })); - } } diff --git a/lib/serve/serveApplicationConstruct.ts b/lib/serve/serveApplicationConstruct.ts index d7179b111..6696eb9b5 100644 --- a/lib/serve/serveApplicationConstruct.ts +++ b/lib/serve/serveApplicationConstruct.ts @@ -19,8 +19,8 @@ import { Credentials, DatabaseInstance, DatabaseInstanceEngine } from 'aws-cdk-l import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { Code, Function, IFunction, ILayerVersion, LayerVersion } from 'aws-cdk-lib/aws-lambda'; -import { ICluster } from 'aws-cdk-lib/aws-ecs'; import { FastApiContainer } from '../api-base/fastApiContainer'; +import { ECSCluster } from '../api-base/ecsCluster'; import { createCdkId } from '../core/utils'; import { Vpc } from '../networking/vpc'; import { BaseProps, Config } from '../schema'; @@ -41,7 +41,6 @@ import { AwsCustomResource, PhysicalResourceId } from 'aws-cdk-lib/custom-resour import { getDefaultRuntime } from '../api-base/utils'; import { ISecurityGroup, Port } from 'aws-cdk-lib/aws-ec2'; import { ECSTasks } from '../api-base/ecsCluster'; -import { letIfDefined } from '../util/common-functions'; import { EventBus } from 'aws-cdk-lib/aws-events'; export type LisaServeApplicationProps = { @@ -58,9 +57,8 @@ export class LisaServeApplicationConstruct extends Construct { public readonly modelsPs: StringParameter; public readonly endpointUrl: StringParameter; public readonly tokenTable?: ITable; - public readonly ecsCluster: ICluster; - public readonly loadBalancer: any; - public readonly listener: any; + public readonly ecsCluster: ECSCluster; + public readonly managementKeySecretName: string; /** * @param {Stack} scope - The parent or owner of the construct. @@ -87,6 +85,9 @@ export class LisaServeApplicationConstruct extends Construct { } this.tokenTable = tokenTable; + const { managementKeySecretName } = this.createManagementKeySecret(scope, config, vpc, securityGroups); + this.managementKeySecretName = managementKeySecretName; + // Create REST API const restApi = new FastApiContainer(scope, 'RestApi', { apiName: 'REST', @@ -95,78 +96,7 @@ export class LisaServeApplicationConstruct extends Construct { securityGroup: vpc.securityGroups.restApiAlbSg, tokenTable: tokenTable, vpc: vpc, - }); - - // Create EventBus for management key rotation events - const managementEventBus = new EventBus(scope, createCdkId([scope.node.id, 'managementEventBus']), { - eventBusName: `${config.deploymentName}-lisa-management-events`, - }); - - // Use a stable name for the management key secret - const managementKeySecret = new Secret(scope, createCdkId([scope.node.id, 'managementKeySecret']), { - secretName: `${config.deploymentName}-lisa-management-key`, // Use stable name based on deployment - description: 'LISA management key secret', - generateSecretString: { - excludePunctuation: true, - passwordLength: 16 - }, - removalPolicy: config.removalPolicy - }); - - // Add rotation policy for the management key secret - const rotationLambda = new Function(scope, createCdkId([scope.node.id, 'managementKeyRotationLambda']), { - runtime: getDefaultRuntime(), - handler: 'management_key.handler', - code: Code.fromAsset(config.lambdaPath || LAMBDA_PATH), - timeout: Duration.minutes(5), - environment: { - EVENT_BUS_NAME: managementEventBus.eventBusName, - }, - role: new Role(scope, createCdkId([scope.node.id, 'managementKeyRotationRole']), { - assumedBy: new ServicePrincipal('lambda.amazonaws.com'), - managedPolicies: [ - ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'), - ], - inlinePolicies: { - 'SecretsManagerRotation': new PolicyDocument({ - statements: [ - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'secretsmanager:DescribeSecret', - 'secretsmanager:GetSecretValue', - 'secretsmanager:PutSecretValue', - 'secretsmanager:UpdateSecretVersionStage' - ], - resources: [managementKeySecret.secretArn] - }), - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'events:PutEvents' - ], - resources: [managementEventBus.eventBusArn] - }) - ] - }) - } - }), - securityGroups: securityGroups, - vpc: vpc.vpc, - }); - - // Configure automatic rotation every 30 days - managementKeySecret.addRotationSchedule('RotationSchedule', { - automaticallyAfter: Duration.days(30), - rotationLambda: rotationLambda - }); - - const managementKeySecretNameStringParameter = new StringParameter(scope, createCdkId(['ManagementKeySecretName']), { - parameterName: `${config.deploymentPrefix}/managementKeySecretName`, - stringValue: managementKeySecret.secretName, - }); - restApi.containers.forEach((container) => { - container.addEnvironment('MANAGEMENT_KEY_NAME', managementKeySecretNameStringParameter.stringValue); + managementKeyName: managementKeySecretName }); // LiteLLM requires a PostgreSQL database to support multiple-instance scaling with dynamic model management. @@ -245,14 +175,27 @@ export class LisaServeApplicationConstruct extends Construct { ...(config.iamRdsAuth ? {} : { passwordSecretId: litellmDbPasswordSecret.secretName }) })); - Object.values(restApi.taskRoles).forEach((taskRole) => { - litellmDbConnectionInfoPs.grantRead(taskRole); - }); - // update the rdsConfig with the endpoint address config.restApiConfig.rdsConfig.dbHost = litellmDb.dbInstanceEndpointAddress; - letIfDefined(restApi.taskRoles[ECSTasks.REST], (serveRole) => { + // Create Parameter Store entry with RestAPI URI + this.endpointUrl = new StringParameter(scope, createCdkId(['LisaServeRestApiUri', 'StringParameter']), { + parameterName: `${config.deploymentPrefix}/lisaServeRestApiUri`, + stringValue: restApi.endpoint, + description: 'URI for LISA Serve API', + }); + + // Create Parameter Store entry with registeredModels + this.modelsPs = new StringParameter(scope, createCdkId(['RegisteredModels', 'StringParameter']), { + parameterName: `${config.deploymentPrefix}/registeredModels`, + stringValue: JSON.stringify([]), + description: 'Serialized JSON of registered models data', + }); + + const serveRole = restApi.apiCluster.taskRoles[ECSTasks.REST]; + if (serveRole) { + // Grant access to REST API task role only + litellmDbConnectionInfoPs.grantRead(serveRole); if (config.iamRdsAuth) { litellmDb.grantConnect(serveRole, serveRole.roleName); @@ -281,56 +224,23 @@ export class LisaServeApplicationConstruct extends Construct { litellmDb.grantConnect(serveRole); litellmDbPasswordSecret.grantRead(serveRole); } - }); - - restApi.containers.forEach((container) => { - container.addEnvironment('LITELLM_DB_INFO_PS_NAME', litellmDbConnectionInfoPs.parameterName); - }); - - if (config.region.includes('iso')) { - const ca_bundle = config.certificateAuthorityBundle ?? ''; - - restApi.containers.forEach((container) => { - container.addEnvironment('SSL_CERT_DIR', '/etc/pki/tls/certs'); - container.addEnvironment('SSL_CERT_FILE', ca_bundle); - container.addEnvironment('REQUESTS_CA_BUNDLE', ca_bundle); - container.addEnvironment('CURL_CA_BUNDLE', ca_bundle); - container.addEnvironment('AWS_CA_BUNDLE', ca_bundle); - }); - } - - // Create Parameter Store entry with RestAPI URI - this.endpointUrl = new StringParameter(scope, createCdkId(['LisaServeRestApiUri', 'StringParameter']), { - parameterName: `${config.deploymentPrefix}/lisaServeRestApiUri`, - stringValue: restApi.endpoint, - description: 'URI for LISA Serve API', - }); - - // Create Parameter Store entry with registeredModels - this.modelsPs = new StringParameter(scope, createCdkId(['RegisteredModels', 'StringParameter']), { - parameterName: `${config.deploymentPrefix}/registeredModels`, - stringValue: JSON.stringify([]), - description: 'Serialized JSON of registered models data', - }); - - letIfDefined(restApi.taskRoles[ECSTasks.REST], (serveRole) => { this.modelsPs.grantRead(serveRole); - }); + } // Add parameter as container environment variable for both RestAPI and RagAPI - restApi.containers.forEach((container) => { + const container = restApi.apiCluster.containers[ECSTasks.REST]; + if (container) { + container.addEnvironment('LITELLM_DB_INFO_PS_NAME', litellmDbConnectionInfoPs.parameterName); container.addEnvironment('REGISTERED_MODELS_PS_NAME', this.modelsPs.parameterName); container.addEnvironment('LITELLM_DB_INFO_PS_NAME', litellmDbConnectionInfoPs.parameterName); - }); + } restApi.node.addDependency(this.modelsPs); restApi.node.addDependency(litellmDbConnectionInfoPs); restApi.node.addDependency(this.endpointUrl); // Update this.restApi = restApi; - this.ecsCluster = restApi.ecsCluster; - this.loadBalancer = restApi.loadBalancer; - this.listener = restApi.listener; + this.ecsCluster = restApi.apiCluster; // Grant permissions after restApi is fully constructed // Additional permissions for REST API Role @@ -360,11 +270,10 @@ export class LisaServeApplicationConstruct extends Construct { }); // Grant SSM parameter read access and attach invocation permissions - const restRole = restApi.taskRoles[ECSTasks.REST]; - if (restRole) { - this.modelsPs.grantRead(restRole); - litellmDbConnectionInfoPs.grantRead(restRole); - restRole.attachInlinePolicy(invocation_permissions); + if (serveRole) { + this.modelsPs.grantRead(serveRole); + litellmDbConnectionInfoPs.grantRead(serveRole); + serveRole.attachInlinePolicy(invocation_permissions); } } @@ -419,4 +328,73 @@ export class LisaServeApplicationConstruct extends Construct { StringParameter.valueForStringParameter(scope, `${config.deploymentPrefix}/layerVersion/common`), ); } + + private createManagementKeySecret (scope: Stack, config: Config, vpc: Vpc, securityGroups: ISecurityGroup[]): { managementKeySecretName: string } { + const managementKeySecretName = `${config.deploymentName}-lisa-management-key`; + + const managementEventBus = new EventBus(scope, createCdkId([scope.node.id, 'managementEventBus']), { + eventBusName: `${config.deploymentName}-lisa-management-events`, + }); + + const managementKeySecret = new Secret(scope, createCdkId([scope.node.id, 'managementKeySecret']), { + secretName: managementKeySecretName, + description: 'LISA management key secret', + generateSecretString: { + excludePunctuation: true, + passwordLength: 16 + }, + removalPolicy: config.removalPolicy + }); + + const rotationLambda = new Function(scope, createCdkId([scope.node.id, 'managementKeyRotationLambda']), { + runtime: getDefaultRuntime(), + handler: 'management_key.handler', + code: Code.fromAsset(config.lambdaPath || LAMBDA_PATH), + timeout: Duration.minutes(5), + environment: { + EVENT_BUS_NAME: managementEventBus.eventBusName, + }, + role: new Role(scope, createCdkId([scope.node.id, 'managementKeyRotationRole']), { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'), + ], + inlinePolicies: { + 'SecretsManagerRotation': new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'secretsmanager:DescribeSecret', + 'secretsmanager:GetSecretValue', + 'secretsmanager:PutSecretValue', + 'secretsmanager:UpdateSecretVersionStage' + ], + resources: [managementKeySecret.secretArn] + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['events:PutEvents'], + resources: [managementEventBus.eventBusArn] + }) + ] + }) + } + }), + securityGroups: securityGroups, + vpc: vpc.vpc, + }); + + managementKeySecret.addRotationSchedule('RotationSchedule', { + automaticallyAfter: Duration.days(30), + rotationLambda: rotationLambda + }); + + new StringParameter(scope, createCdkId(['ManagementKeySecretName']), { + parameterName: `${config.deploymentPrefix}/managementKeySecretName`, + stringValue: managementKeySecret.secretName, + }); + + return { managementKeySecretName }; + } } diff --git a/lib/stages.ts b/lib/stages.ts index e66da0d8f..2c772d67f 100644 --- a/lib/stages.ts +++ b/lib/stages.ts @@ -303,10 +303,9 @@ export class LisaServeApplicationStage extends Stage { restApiId: apiBaseStack.restApiId, rootResourceId: apiBaseStack.rootResourceId, authorizerId: apiBaseStack.authorizer?.authorizerId || '', - ecsCluster: serveStack.ecsCluster, - loadBalancer: serveStack.loadBalancer, - listener: serveStack.listener, + apiCluster: serveStack.restApi.apiCluster, }); + mcpWorkbenchStack.addDependency(coreStack); mcpWorkbenchStack.addDependency(apiBaseStack); mcpWorkbenchStack.addDependency(serveStack); apiDeploymentStack.addDependency(mcpWorkbenchStack); diff --git a/scripts/generate-baseline.sh b/scripts/generate-baseline.sh new file mode 100755 index 000000000..9c35f38d8 --- /dev/null +++ b/scripts/generate-baseline.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Generate baseline templates from MockApp + +set -e + +RELEASE_TAG=${1:-$(git describe --tags --abbrev=0)} +BASELINE_DIR="test/cdk/stacks/__baselines__" + +echo "Generating baselines from release: $RELEASE_TAG" + +# Stash current changes +git stash push -m "Temporary stash for baseline generation" + +# Checkout release tag +git checkout "$RELEASE_TAG" + +# Install dependencies and build +npm ci +npm run build + +# Remove existing baselines to force regeneration +rm -rf "$BASELINE_DIR" +mkdir -p "$BASELINE_DIR" + +# Run snapshot tests which will generate baselines from MockApp +npm test -- test/cdk/stacks/snapshot.test.ts --testNamePattern="is compatible with baseline" + +# Return to previous branch +git checkout - + +# Restore stashed changes +git stash pop || true + +echo "Baselines generated in $BASELINE_DIR" diff --git a/test/cdk/mocks/ConfigParser.ts b/test/cdk/mocks/ConfigParser.ts index 756b0a162..9b884b04a 100644 --- a/test/cdk/mocks/ConfigParser.ts +++ b/test/cdk/mocks/ConfigParser.ts @@ -43,10 +43,11 @@ export default class ConfigParser { } catch (error) { if (error instanceof Error) { console.error('Error parsing the configuration:', error.message); + throw new Error(`Configuration parsing failed: ${error.message}`); } else { console.error('An unexpected error occurred:', error); + throw new Error('Configuration parsing failed: An unexpected error occurred'); } - process.exit(1); } return config; } diff --git a/test/cdk/mocks/MockApp.ts b/test/cdk/mocks/MockApp.ts index 38b96b4b0..9590e1e63 100644 --- a/test/cdk/mocks/MockApp.ts +++ b/test/cdk/mocks/MockApp.ts @@ -33,6 +33,7 @@ import { LisaRagStack } from '../../../lib/rag'; import fs from 'node:fs'; import { DOCS_DIST_PATH, ECS_MODEL_DEPLOYER_DIST_PATH, VECTOR_STORE_DEPLOYER_DIST_PATH, WEBAPP_DIST_PATH } from '../../../lib/util'; +const TEST_LAYER_DIR = './test/cdk/mocks/layers'; export default class MockApp { private static mockApp: any; @@ -71,7 +72,7 @@ export default class MockApp { }; // Create dist folders to ensure stack creation - [VECTOR_STORE_DEPLOYER_DIST_PATH, ECS_MODEL_DEPLOYER_DIST_PATH, DOCS_DIST_PATH, WEBAPP_DIST_PATH].forEach((distFolder) => + [VECTOR_STORE_DEPLOYER_DIST_PATH, ECS_MODEL_DEPLOYER_DIST_PATH, DOCS_DIST_PATH, WEBAPP_DIST_PATH, TEST_LAYER_DIR].forEach((distFolder) => fs.mkdirSync(distFolder, { recursive: true }) ); @@ -166,12 +167,10 @@ export default class MockApp { restApiId: apiBaseStack.restApiId, rootResourceId: apiBaseStack.rootResourceId, authorizerId: apiBaseStack.authorizer?.authorizerId || '', - ecsCluster: serveStack.ecsCluster, - loadBalancer: serveStack.loadBalancer, - listener: serveStack.listener, + apiCluster: serveStack.restApi.apiCluster }); - const stacks = [ + const stacks: cdk.Stack[] = [ networkingStack, iamStack, apiBaseStack, diff --git a/test/cdk/mocks/assets.yaml b/test/cdk/mocks/assets.yaml new file mode 100644 index 000000000..62114488b --- /dev/null +++ b/test/cdk/mocks/assets.yaml @@ -0,0 +1,26 @@ +env: dev + +dev: + lambdaLayerAssets: + authorizerLayerPath: ./test/cdk/mocks/layers + commonLayerPath: ./test/cdk/mocks/layers + fastapiLayerPath: ./test/cdk/mocks/layers + ragLayerPath: ./test/cdk/mocks/layers + restApiConfig: + imageConfig: + type: ecr + repositoryArn: arn:aws:ecr:us-east-1:123456789012:repository/test-api + tag: latest + mcpWorkbenchConfig: + type: ecr + repositoryArn: arn:aws:ecr:us-east-1:123456789012:repository/test-mcp + tag: latest + batchIngestionConfig: + type: ecr + repositoryArn: arn:aws:ecr:us-east-1:123456789012:repository/test-batch + tag: latest + webAppAssetsPath: ./test/cdk/mocks/ui + ecsModelDeployerPath: ./ecs_model_deployer + vectorStoreDeployerPath: ./vector_store_deployer + documentsPath: ./test/cdk/mocks/ui + lambdaPath: ./lambda diff --git a/test/cdk/mocks/config-test.yaml b/test/cdk/mocks/config-test.yaml index ac723f4a1..7713b12a6 100644 --- a/test/cdk/mocks/config-test.yaml +++ b/test/cdk/mocks/config-test.yaml @@ -42,49 +42,7 @@ dev: logLevel: DEBUG restApiConfig: sslCertIamArn: arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev - ragRepositories: - - repositoryId: pgvector-rag - repositoryName: "PGVector" - type: pgvector - rdsConfig: - username: postgres - - repositoryId: pgvector-rag2 - repositoryName: "PGVector 2" - type: pgvector - rdsConfig: - username: postgres - allowedGroups: - - group1 - - repositoryId: opensearch-rag - type: opensearch - repositoryName: "Opensearch 2" - opensearchConfig: - dataNodes: 2 - dataNodeInstanceType: r7g.large.search - masterNodes: 0 - masterNodeInstanceType: r7g.large.search - volumeSize: 20 - multiAzWithStandby: false - # - repositoryId: default - # type: opensearch - # opensearchConfig: - # dataNodes: 2 - # dataNodeInstanceType: r6g.large.search - # masterNodes: 0 - # masterNodeInstanceType: r6g.large.search - # volumeSize: 300 - # If adding an existing PGVector database, this configurations assumes: - # 1. The database has been configured to have pgvector installed and enabled: https://aws.amazon.com/about-aws/whats-new/2023/05/amazon-rds-postgresql-pgvector-ml-model-integration/ - # 2. The database is accessible by RAG-related lambda functions (add inbound PostgreSQL access on the database's security group for all Lambda RAG security groups) - # 3. A secret ID exists in SecretsManager holding the database password within a json block of '{"password":"your_password_here"}'. This is the same format that RDS natively provides a password in SecretsManager. - # If the passwordSecretId or dbHost are not provided, then a sample database will be created for you. Only the username is required. - # - repositoryId: pgvector-rag - # type: pgvector - # rdsConfig: - # username: postgres - # passwordSecretId: # password ID as stored in SecretsManager. Example: "rds!db-aa88493d-be8d-4a3f-96dc-c668165f7826" - # dbHost: # Host name of database. Example hostname from RDS: "my-db-name.291b2f03.us-east-1.rds.amazonaws.com" - # dbName: postgres + ragRepositories: [] ragFileProcessingConfig: chunkSize: 512 chunkOverlap: 51 diff --git a/test/cdk/stacks/README.md b/test/cdk/stacks/README.md new file mode 100644 index 000000000..7de157f7d --- /dev/null +++ b/test/cdk/stacks/README.md @@ -0,0 +1,47 @@ +# Migration Testing + +## Overview + +The snapshot tests compare current stack templates against baseline templates from previous releases to detect breaking changes. + +## Usage + +### 1. Generate Baselines from Previous Release + +```bash +# Generate from latest release tag +./scripts/generate-baseline.sh + +# Generate from specific release +./scripts/generate-baseline.sh v5.3.0 +``` + +### 2. Run Migration Tests + +```bash +npm test -- test/cdk/stacks/snapshot.test.ts +``` + +### 3. Review Failures + +If tests fail, they'll report: +- Removed resources (potential data loss) +- Changed resource types (will cause replacement) + +### 4. Update Baselines After Approved Changes + +```bash +# Regenerate baselines from current code +npm test -- test/cdk/stacks/snapshot.test.ts +``` + +## CI/CD Integration + +Add to your pipeline: + +```yaml +- name: Migration Test + run: | + ./scripts/generate-baseline.sh ${{ github.event.pull_request.base.ref }} + npm test -- test/cdk/stacks/snapshot.test.ts +``` diff --git a/test/cdk/stacks/__baselines__/LisaApiBase.json b/test/cdk/stacks/__baselines__/LisaApiBase.json new file mode 100644 index 000000000..b272419c0 --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaApiBase.json @@ -0,0 +1,507 @@ +{ + "Parameters": { + "LisaApiAuthorizerLisaApiAuthorizermanagementKeyStringParameterParameter5998CD79": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/managementKeySecretName" + }, + "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/layerVersion/common" + }, + "SsmParameterValuedevtestlisalisalayerVersionauthorizerC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/layerVersion/authorizer" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Resources": { + "LisaApiAuthorizerAuthorizerLambdaServiceRoleFC9BFB65": { + "Type": "AWS::IAM::Role", + "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" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ] + } + }, + "LisaApiAuthorizerAuthorizerLambdaServiceRoleDefaultPolicy291388A2": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::ImportValue": "LisaServe:ExportsOutputFnGetAttTokenTable3625D248ArnC9157255" + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":secretsmanager:us-iso-east-1:012345678901:secret:", + { + "Ref": "LisaApiAuthorizerLisaApiAuthorizermanagementKeyStringParameterParameter5998CD79" + }, + "-??????" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LisaApiAuthorizerAuthorizerLambdaServiceRoleDefaultPolicy291388A2", + "Roles": [ + { + "Ref": "LisaApiAuthorizerAuthorizerLambdaServiceRoleFC9BFB65" + } + ] + } + }, + "LisaApiAuthorizerAuthorizerLambdaF64E36CD": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "REST API and UI Authorization Lambda", + "Environment": { + "Variables": { + "CLIENT_ID": "test", + "AUTHORITY": "test", + "ADMIN_GROUP": "", + "USER_GROUP": "", + "JWT_GROUPS_PROP": "", + "MANAGEMENT_KEY_NAME": { + "Ref": "LisaApiAuthorizerLisaApiAuthorizermanagementKeyStringParameterParameter5998CD79" + }, + "TOKEN_TABLE_NAME": { + "Fn::ImportValue": "LisaServe:ExportsOutputRefTokenTable3625D248A47BF86E" + } + } + }, + "FunctionName": "LisaApiBase-lambda-authorizer", + "Handler": "authorizer.lambda_functions.lambda_handler", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionauthorizerC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "LisaApiAuthorizerAuthorizerLambdaServiceRoleFC9BFB65", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 30, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaApiAuthorizerAuthorizerLambdaServiceRoleDefaultPolicy291388A2", + "LisaApiAuthorizerAuthorizerLambdaServiceRoleFC9BFB65" + ] + }, + "LisaApiAuthorizerAuthorizerLambdaLisaApiBaseLisaApiAuthorizerAPIGWAuthorizer0706D1FFPermissions026E08A4": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LisaApiAuthorizerAuthorizerLambdaF64E36CD", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Ref": "LisaApiBaseRestApiBD7EFE41" + }, + "/authorizers/", + { + "Ref": "LisaApiAuthorizerAPIGWAuthorizerE7001880" + } + ] + ] + } + } + }, + "LisaApiAuthorizerAPIGWAuthorizerE7001880": { + "Type": "AWS::ApiGateway::Authorizer", + "Properties": { + "AuthorizerResultTtlInSeconds": 0, + "AuthorizerUri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "LisaApiAuthorizerAuthorizerLambdaF64E36CD", + "Arn" + ] + } + ] + } + ] + }, + ":apigateway:", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "LisaApiAuthorizerAuthorizerLambdaF64E36CD", + "Arn" + ] + } + ] + } + ] + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LisaApiAuthorizerAuthorizerLambdaF64E36CD", + "Arn" + ] + }, + "/invocations" + ] + ] + }, + "IdentitySource": "method.request.header.Authorization", + "Name": "LisaApiBaseLisaApiAuthorizerAPIGWAuthorizer0706D1FF", + "RestApiId": { + "Ref": "LisaApiBaseRestApiBD7EFE41" + }, + "Type": "REQUEST" + } + }, + "LisaApiBaseRestApiBD7EFE41": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "BinaryMediaTypes": [ + "font/*", + "image/*" + ], + "Description": "Base API Gateway for LISA.", + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Name": "LisaApiBase-RestApi" + } + }, + "LisaApiBaseRestApiCloudWatchRoleC64221F2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LisaApiBaseRestApiAccount63CB3D5A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "LisaApiBaseRestApiCloudWatchRoleC64221F2", + "Arn" + ] + } + }, + "DependsOn": [ + "LisaApiBaseRestApiBD7EFE41" + ], + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LisaApiBaseRestApiDeployment0EFEF450a469aadc1179465edb3265abd2173fed": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "Base API Gateway for LISA.", + "RestApiId": { + "Ref": "LisaApiBaseRestApiBD7EFE41" + } + }, + "DependsOn": [ + "LisaApiBaseRestApiOPTIONSB08FDC9B" + ], + "Metadata": { + "aws:cdk:do-not-refactor": true + } + }, + "LisaApiBaseRestApiDeploymentStagedev221E5167": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "LisaApiBaseRestApiDeployment0EFEF450a469aadc1179465edb3265abd2173fed" + }, + "MethodSettings": [ + { + "DataTraceEnabled": false, + "HttpMethod": "*", + "ResourcePath": "/*", + "ThrottlingBurstLimit": 100, + "ThrottlingRateLimit": 100 + } + ], + "RestApiId": { + "Ref": "LisaApiBaseRestApiBD7EFE41" + }, + "StageName": "dev" + }, + "DependsOn": [ + "LisaApiBaseRestApiAccount63CB3D5A" + ] + }, + "LisaApiBaseRestApiOPTIONSB08FDC9B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Fn::GetAtt": [ + "LisaApiBaseRestApiBD7EFE41", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "LisaApiBaseRestApiBD7EFE41" + } + } + } + }, + "Outputs": { + "LisaApiBaseRestApiEndpoint24445B65": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "LisaApiBaseRestApiBD7EFE41" + }, + ".execute-api.us-iso-east-1.", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "LisaApiBaseRestApiDeploymentStagedev221E5167" + }, + "/" + ] + ] + } + }, + "ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A": { + "Value": { + "Fn::GetAtt": [ + "LisaApiBaseRestApiBD7EFE41", + "RootResourceId" + ] + }, + "Export": { + "Name": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + } + }, + "ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C": { + "Value": { + "Ref": "LisaApiBaseRestApiBD7EFE41" + }, + "Export": { + "Name": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + }, + "ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB": { + "Value": { + "Ref": "LisaApiAuthorizerAPIGWAuthorizerE7001880" + }, + "Export": { + "Name": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + } + } + }, + "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/test/cdk/stacks/__baselines__/LisaApiDeployment.json b/test/cdk/stacks/__baselines__/LisaApiDeployment.json new file mode 100644 index 000000000..69e58d199 --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaApiDeployment.json @@ -0,0 +1,103 @@ +{ + "Resources": { + "Deployment176160239998183A70CE5": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "StageName": "dev" + }, + "Metadata": { + "aws:cdk:do-not-refactor": true + } + }, + "LisaApiDeploymentStringParameter82AB29E6": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "API Gateway URL for LISA", + "Name": "/dev/test-lisa/lisa/LisaApiUrl", + "Type": "String", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/dev" + ] + ] + } + } + } + }, + "Outputs": { + "ApiUrl": { + "Description": "API Gateway URL", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/dev" + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "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/test/cdk/stacks/__baselines__/LisaChat.json b/test/cdk/stacks/__baselines__/LisaChat.json new file mode 100644 index 000000000..f1d6541fd --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaChat.json @@ -0,0 +1,5654 @@ +{ + "Resources": { + "McpApiMcpServersTable96D5EF7B": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + }, + { + "AttributeName": "owner", + "AttributeType": "S" + }, + { + "AttributeName": "created", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "GlobalSecondaryIndexes": [ + { + "IndexName": "byOwner", + "KeySchema": [ + { + "AttributeName": "owner", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "byOwnerSorted", + "KeySchema": [ + { + "AttributeName": "owner", + "KeyType": "HASH" + }, + { + "AttributeName": "created", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + }, + { + "AttributeName": "owner", + "KeyType": "RANGE" + } + ], + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "McpApiMcpServersTableNameParameter40CBF850": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Name of the MCP servers DynamoDB table", + "Name": "/dev/test-lisa/lisa/table/mcpServersTable", + "Type": "String", + "Value": { + "Ref": "McpApiMcpServersTable96D5EF7B" + } + } + }, + "McpApiRestApimcpserver98B9AA02": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "mcp-server", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpApiRestApimcpserverOPTIONSBDFFD30B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "McpApiRestApimcpserver98B9AA02" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpApiRestApimcpserverGETApiPermissionLisaChatMcpApiRestApi21151341GETmcpserver6BDDE98F": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverlist6AA9C7D0", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/mcp-server" + ] + ] + } + } + }, + "McpApiRestApimcpserverGETApiPermissionTestLisaChatMcpApiRestApi21151341GETmcpserver126C039E": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverlist6AA9C7D0", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/mcp-server" + ] + ] + } + } + }, + "McpApiRestApimcpserverGETBE58B913": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverlist6AA9C7D0", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpApiRestApimcpserver98B9AA02" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpApiRestApimcpserverserverId53F0C4B8": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "McpApiRestApimcpserver98B9AA02" + }, + "PathPart": "{serverId}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpApiRestApimcpserverserverIdOPTIONSB7066F81": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "McpApiRestApimcpserverserverId53F0C4B8" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpApiRestApimcpserverserverIdGETApiPermissionLisaChatMcpApiRestApi21151341GETmcpserverserverIdA3F6D4A1": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverget2921E1D3", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/mcp-server/*" + ] + ] + } + } + }, + "McpApiRestApimcpserverserverIdGETApiPermissionTestLisaChatMcpApiRestApi21151341GETmcpserverserverId2CCBA2B7": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverget2921E1D3", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/mcp-server/*" + ] + ] + } + } + }, + "McpApiRestApimcpserverserverIdGETD8368189": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverget2921E1D3", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpApiRestApimcpserverserverId53F0C4B8" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpApiRestApimcpserverserverIdDELETEApiPermissionLisaChatMcpApiRestApi21151341DELETEmcpserverserverIdB3F749E4": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverdeleteB1B6EC65", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/DELETE/mcp-server/*" + ] + ] + } + } + }, + "McpApiRestApimcpserverserverIdDELETEApiPermissionTestLisaChatMcpApiRestApi21151341DELETEmcpserverserverId971619E5": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverdeleteB1B6EC65", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/DELETE/mcp-server/*" + ] + ] + } + } + }, + "McpApiRestApimcpserverserverIdDELETEC2D3E2DF": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "DELETE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverdeleteB1B6EC65", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpApiRestApimcpserverserverId53F0C4B8" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpApiRestApimcpserverserverIdPUTApiPermissionLisaChatMcpApiRestApi21151341PUTmcpserverserverId12A5A0C6": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverupdate2F094A10", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/PUT/mcp-server/*" + ] + ] + } + } + }, + "McpApiRestApimcpserverserverIdPUTApiPermissionTestLisaChatMcpApiRestApi21151341PUTmcpserverserverIdDE097B6B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverupdate2F094A10", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/PUT/mcp-server/*" + ] + ] + } + } + }, + "McpApiRestApimcpserverserverIdPUT03827812": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "PUT", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpApiLisaChatmcpserverupdate2F094A10", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpApiRestApimcpserverserverId53F0C4B8" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpApiRestApimcpserverPOSTApiPermissionLisaChatMcpApiRestApi21151341POSTmcpserver091916E8": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpApiLisaChatmcpservercreate8CAC0C43", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/POST/mcp-server" + ] + ] + } + } + }, + "McpApiRestApimcpserverPOSTApiPermissionTestLisaChatMcpApiRestApi21151341POSTmcpserver376E6365": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpApiLisaChatmcpservercreate8CAC0C43", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/POST/mcp-server" + ] + ] + } + } + }, + "McpApiRestApimcpserverPOST54760652": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "POST", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpApiLisaChatmcpservercreate8CAC0C43", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpApiRestApimcpserver98B9AA02" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Role used by LISA McpServerApi lambdas to access AWS resources", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable", + "dynamodb:GetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "McpApiMcpServersTable96D5EF7B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "McpApiMcpServersTable96D5EF7B", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "lambdaPermissions" + } + ], + "RoleName": "test-lisa-LisaMcpServerApiLambdaExecutionRole" + } + }, + "McpApiLisaMcpServerApiLambdaExecutionRoleDefaultPolicy53645ED0": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "McpApiMcpServersTable96D5EF7B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "McpApiMcpServersTable96D5EF7B", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": [ + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "McpApiMcpServersTable96D5EF7B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "McpApiMcpServersTable96D5EF7B", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "McpApiMcpServersTable96D5EF7B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "McpApiMcpServersTable96D5EF7B", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "McpApiLisaMcpServerApiLambdaExecutionRoleDefaultPolicy53645ED0", + "Roles": [ + { + "Ref": "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F" + } + ] + } + }, + "McpApiLisaChatmcpserverlist6AA9C7D0": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Lists available mcp servers for user", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "MCP_SERVERS_TABLE_NAME": { + "Ref": "McpApiMcpServersTable96D5EF7B" + }, + "MCP_SERVERS_BY_OWNER_INDEX_NAME": "byOwner" + } + }, + "FunctionName": "LisaChat-mcp_server-list", + "Handler": "mcp_server.lambda_functions.list", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpApiLisaMcpServerApiLambdaExecutionRoleDefaultPolicy53645ED0", + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F" + ] + }, + "McpApiLisaChatmcpserverget2921E1D3": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Returns the selected mcp server", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "MCP_SERVERS_TABLE_NAME": { + "Ref": "McpApiMcpServersTable96D5EF7B" + }, + "MCP_SERVERS_BY_OWNER_INDEX_NAME": "byOwner" + } + }, + "FunctionName": "LisaChat-mcp_server-get", + "Handler": "mcp_server.lambda_functions.get", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpApiLisaMcpServerApiLambdaExecutionRoleDefaultPolicy53645ED0", + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F" + ] + }, + "McpApiLisaChatmcpservercreate8CAC0C43": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Creates the mcp server", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "MCP_SERVERS_TABLE_NAME": { + "Ref": "McpApiMcpServersTable96D5EF7B" + }, + "MCP_SERVERS_BY_OWNER_INDEX_NAME": "byOwner" + } + }, + "FunctionName": "LisaChat-mcp_server-create", + "Handler": "mcp_server.lambda_functions.create", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpApiLisaMcpServerApiLambdaExecutionRoleDefaultPolicy53645ED0", + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F" + ] + }, + "McpApiLisaChatmcpserverdeleteB1B6EC65": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Deletes selected mcp server", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "MCP_SERVERS_TABLE_NAME": { + "Ref": "McpApiMcpServersTable96D5EF7B" + }, + "MCP_SERVERS_BY_OWNER_INDEX_NAME": "byOwner" + } + }, + "FunctionName": "LisaChat-mcp_server-delete", + "Handler": "mcp_server.lambda_functions.delete", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpApiLisaMcpServerApiLambdaExecutionRoleDefaultPolicy53645ED0", + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F" + ] + }, + "McpApiLisaChatmcpserverupdate2F094A10": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Creates or updates selected mcp server", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "MCP_SERVERS_TABLE_NAME": { + "Ref": "McpApiMcpServersTable96D5EF7B" + }, + "MCP_SERVERS_BY_OWNER_INDEX_NAME": "byOwner" + } + }, + "FunctionName": "LisaChat-mcp_server-update", + "Handler": "mcp_server.lambda_functions.update", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpApiLisaMcpServerApiLambdaExecutionRoleDefaultPolicy53645ED0", + "McpApiLisaMcpServerApiLambdaExecutionRoleAB722D7F" + ] + }, + "ConfigurationApiConfigurationTable4B2B7EE1": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "configScope", + "AttributeType": "S" + }, + { + "AttributeName": "versionId", + "AttributeType": "N" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "configScope", + "KeyType": "HASH" + }, + { + "AttributeName": "versionId", + "KeyType": "RANGE" + } + ], + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ConfigurationApiLisaConfigurationApiLambdaExecutionRole52653FA6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Role used by LISA ConfigurationApi lambdas to access AWS resources", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable", + "dynamodb:GetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ConfigurationApiConfigurationTable4B2B7EE1", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ConfigurationApiConfigurationTable4B2B7EE1", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "lambdaPermissions" + } + ], + "RoleName": "test-lisa-LisaConfigurationApiLambdaExecutionRole" + } + }, + "ConfigurationApiLisaConfigurationApiLambdaExecutionRoleDefaultPolicy61BCD4DD": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:us-iso-east-1:012345678901:table/", + { + "Fn::GetAtt": [ + "McpApiMcpServersTableNameParameter40CBF850", + "Value" + ] + } + ] + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ConfigurationApiConfigurationTable4B2B7EE1", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Action": [ + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ConfigurationApiConfigurationTable4B2B7EE1", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ConfigurationApiLisaConfigurationApiLambdaExecutionRoleDefaultPolicy61BCD4DD", + "Roles": [ + { + "Ref": "ConfigurationApiLisaConfigurationApiLambdaExecutionRole52653FA6" + } + ] + } + }, + "ConfigurationApilisainitddbconfig1F5E665A": { + "Type": "Custom::AWS", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "Fn::Join": [ + "", + [ + "{\"service\":\"DynamoDB\",\"action\":\"putItem\",\"physicalResourceId\":{\"id\":\"initConfigData\"},\"parameters\":{\"TableName\":\"", + { + "Ref": "ConfigurationApiConfigurationTable4B2B7EE1" + }, + "\",\"Item\":{\"versionId\":{\"N\":\"0\"},\"changedBy\":{\"S\":\"System\"},\"configScope\":{\"S\":\"global\"},\"changeReason\":{\"S\":\"Initial deployment default config\"},\"createdAt\":{\"S\":\"1761602400\"},\"configuration\":{\"M\":{\"enabledComponents\":{\"M\":{\"deleteSessionHistory\":{\"BOOL\":\"True\"},\"viewMetaData\":{\"BOOL\":\"True\"},\"editKwargs\":{\"BOOL\":\"True\"},\"editPromptTemplate\":{\"BOOL\":\"True\"},\"editChatHistoryBuffer\":{\"BOOL\":\"True\"},\"editNumOfRagDocument\":{\"BOOL\":\"True\"},\"uploadRagDocs\":{\"BOOL\":\"True\"},\"uploadContextDocs\":{\"BOOL\":\"True\"},\"documentSummarization\":{\"BOOL\":\"True\"},\"showRagLibrary\":{\"BOOL\":\"True\"},\"showMcpWorkbench\":{\"BOOL\":\"False\"},\"showPromptTemplateLibrary\":{\"BOOL\":\"True\"},\"mcpConnections\":{\"BOOL\":\"True\"},\"modelLibrary\":{\"BOOL\":\"True\"},\"encryptSession\":{\"BOOL\":\"False\"}}},\"systemBanner\":{\"M\":{\"isEnabled\":{\"BOOL\":\"False\"},\"text\":{\"S\":\"\"},\"textColor\":{\"S\":\"\"},\"backgroundColor\":{\"S\":\"\"}}}}}}}}" + ] + ] + }, + "InstallLatestAwsSdk": true + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ConfigurationApiRestApiconfigurationD60D7FC0": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "configuration", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ConfigurationApiRestApiconfigurationOPTIONSE0FA2B6B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "ConfigurationApiRestApiconfigurationD60D7FC0" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ConfigurationApiRestApiconfigurationGETApiPermissionLisaChatConfigurationApiRestApiBDBDE1ADGETconfiguration0A0B9F64": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ConfigurationApiLisaChatconfigurationgetconfiguration19178EAF", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/configuration" + ] + ] + } + } + }, + "ConfigurationApiRestApiconfigurationGETApiPermissionTestLisaChatConfigurationApiRestApiBDBDE1ADGETconfiguration4DDA737D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ConfigurationApiLisaChatconfigurationgetconfiguration19178EAF", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/configuration" + ] + ] + } + } + }, + "ConfigurationApiRestApiconfigurationGETA0E186D1": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "ConfigurationApiLisaChatconfigurationgetconfiguration19178EAF", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "ConfigurationApiRestApiconfigurationD60D7FC0" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ConfigurationApiRestApiconfigurationconfigScope7534F355": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "ConfigurationApiRestApiconfigurationD60D7FC0" + }, + "PathPart": "{configScope}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ConfigurationApiRestApiconfigurationconfigScopeOPTIONSFF7B983C": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "ConfigurationApiRestApiconfigurationconfigScope7534F355" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ConfigurationApiRestApiconfigurationconfigScopePUTApiPermissionLisaChatConfigurationApiRestApiBDBDE1ADPUTconfigurationconfigScope680DC68F": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ConfigurationApiLisaChatconfigurationupdateconfigurationB328F68F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/PUT/configuration/*" + ] + ] + } + } + }, + "ConfigurationApiRestApiconfigurationconfigScopePUTApiPermissionTestLisaChatConfigurationApiRestApiBDBDE1ADPUTconfigurationconfigScope1E1BEE6D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ConfigurationApiLisaChatconfigurationupdateconfigurationB328F68F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/PUT/configuration/*" + ] + ] + } + } + }, + "ConfigurationApiRestApiconfigurationconfigScopePUTA6AA6233": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "PUT", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "ConfigurationApiLisaChatconfigurationupdateconfigurationB328F68F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "ConfigurationApiRestApiconfigurationconfigScope7534F355" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ConfigurationApiLisaChatconfigurationgetconfiguration19178EAF": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Get configuration", + "Environment": { + "Variables": { + "CONFIG_TABLE_NAME": { + "Ref": "ConfigurationApiConfigurationTable4B2B7EE1" + }, + "FASTAPI_ENDPOINT": { + "Ref": "SsmParameterValuedevtestlisalisaserveendpointC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "MCP_SERVERS_TABLE_NAME": { + "Fn::GetAtt": [ + "McpApiMcpServersTableNameParameter40CBF850", + "Value" + ] + } + } + }, + "FunctionName": "LisaChat-configuration-get_configuration", + "Handler": "configuration.lambda_functions.get_configuration", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "ConfigurationApiLisaConfigurationApiLambdaExecutionRole52653FA6", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ConfigurationApiLisaConfigurationApiLambdaExecutionRoleDefaultPolicy61BCD4DD", + "ConfigurationApiLisaConfigurationApiLambdaExecutionRole52653FA6" + ] + }, + "ConfigurationApiLisaChatconfigurationupdateconfigurationB328F68F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Updates config data", + "Environment": { + "Variables": { + "CONFIG_TABLE_NAME": { + "Ref": "ConfigurationApiConfigurationTable4B2B7EE1" + }, + "FASTAPI_ENDPOINT": { + "Ref": "SsmParameterValuedevtestlisalisaserveendpointC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "MCP_SERVERS_TABLE_NAME": { + "Fn::GetAtt": [ + "McpApiMcpServersTableNameParameter40CBF850", + "Value" + ] + } + } + }, + "FunctionName": "LisaChat-configuration-update_configuration", + "Handler": "configuration.lambda_functions.update_configuration", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "ConfigurationApiLisaConfigurationApiLambdaExecutionRole52653FA6", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ConfigurationApiLisaConfigurationApiLambdaExecutionRoleDefaultPolicy61BCD4DD", + "ConfigurationApiLisaConfigurationApiLambdaExecutionRole52653FA6" + ] + }, + "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "34a66902956b031404ef497526f619b900363fe7fd65ff02b1de4c30fe10c034.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "ConfigurationApiLisaConfigurationApiLambdaExecutionRole52653FA6", + "Arn" + ] + }, + "Runtime": "nodejs22.x", + "Timeout": 120 + }, + "DependsOn": [ + "ConfigurationApiLisaConfigurationApiLambdaExecutionRoleDefaultPolicy61BCD4DD", + "ConfigurationApiLisaConfigurationApiLambdaExecutionRole52653FA6" + ] + }, + "SessionApiSessionsTableDA695141": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "sessionId", + "AttributeType": "S" + }, + { + "AttributeName": "userId", + "AttributeType": "S" + }, + { + "AttributeName": "startTime", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "GlobalSecondaryIndexes": [ + { + "IndexName": "byUserId", + "KeySchema": [ + { + "AttributeName": "userId", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "byUserIdSorted", + "KeySchema": [ + { + "AttributeName": "userId", + "KeyType": "HASH" + }, + { + "AttributeName": "startTime", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "KeySchema": [ + { + "AttributeName": "sessionId", + "KeyType": "HASH" + }, + { + "AttributeName": "userId", + "KeyType": "RANGE" + } + ], + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SessionApiSessionEncryptionKeyA03622E8": { + "Type": "AWS::KMS::Key", + "Properties": { + "Description": "KMS key for encrypting session data at rest", + "EnableKeyRotation": true, + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::012345678901:root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SessionApiSessionEncryptionKeyArnParameterB8786542": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/sessionEncryptionKeyArn", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "SessionApiSessionEncryptionKeyA03622E8", + "Arn" + ] + } + } + }, + "SessionApiRestApisessionADA907DB": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "session", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionOPTIONSBBD31823": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "SessionApiRestApisessionADA907DB" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionGETApiPermissionLisaChatSessionApiRestApiDD07298DGETsession74C1CDE3": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionlistsessionsB88A4A44", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/session" + ] + ] + } + } + }, + "SessionApiRestApisessionGETApiPermissionTestLisaChatSessionApiRestApiDD07298DGETsessionEC01D86D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionlistsessionsB88A4A44", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/session" + ] + ] + } + } + }, + "SessionApiRestApisessionGET0BD1FCA4": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionlistsessionsB88A4A44", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "SessionApiRestApisessionADA907DB" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionId2D6ACCD9": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "SessionApiRestApisessionADA907DB" + }, + "PathPart": "{sessionId}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionIdOPTIONS2308AE82": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "SessionApiRestApisessionsessionId2D6ACCD9" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionIdGETApiPermissionLisaChatSessionApiRestApiDD07298DGETsessionsessionIdE4A5F22B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessiongetsessionBC2FF1E4", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/session/*" + ] + ] + } + } + }, + "SessionApiRestApisessionsessionIdGETApiPermissionTestLisaChatSessionApiRestApiDD07298DGETsessionsessionIdC4F097A5": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessiongetsessionBC2FF1E4", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/session/*" + ] + ] + } + } + }, + "SessionApiRestApisessionsessionIdGET58FB905A": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "SessionApiLisaChatsessiongetsessionBC2FF1E4", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "SessionApiRestApisessionsessionId2D6ACCD9" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionIdDELETEApiPermissionLisaChatSessionApiRestApiDD07298DDELETEsessionsessionIdFC81FEF0": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessiondeletesession88A67D12", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/DELETE/session/*" + ] + ] + } + } + }, + "SessionApiRestApisessionsessionIdDELETEApiPermissionTestLisaChatSessionApiRestApiDD07298DDELETEsessionsessionId6B8AF9A4": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessiondeletesession88A67D12", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/DELETE/session/*" + ] + ] + } + } + }, + "SessionApiRestApisessionsessionIdDELETED0E27EDE": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "DELETE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "SessionApiLisaChatsessiondeletesession88A67D12", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "SessionApiRestApisessionsessionId2D6ACCD9" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionIdPUTApiPermissionLisaChatSessionApiRestApiDD07298DPUTsessionsessionId87E28A4C": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionputsession28059E77", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/PUT/session/*" + ] + ] + } + } + }, + "SessionApiRestApisessionsessionIdPUTApiPermissionTestLisaChatSessionApiRestApiDD07298DPUTsessionsessionId5BB295D9": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionputsession28059E77", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/PUT/session/*" + ] + ] + } + } + }, + "SessionApiRestApisessionsessionIdPUTFC291EB3": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "PUT", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionputsession28059E77", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "SessionApiRestApisessionsessionId2D6ACCD9" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionIdname2D15D080": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "SessionApiRestApisessionsessionId2D6ACCD9" + }, + "PathPart": "name", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionIdnameOPTIONSE93CD5DC": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "SessionApiRestApisessionsessionIdname2D15D080" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionIdnamePUTApiPermissionLisaChatSessionApiRestApiDD07298DPUTsessionsessionIdname31B5E2B2": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionrenamesession93520CEA", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/PUT/session/*/name" + ] + ] + } + } + }, + "SessionApiRestApisessionsessionIdnamePUTApiPermissionTestLisaChatSessionApiRestApiDD07298DPUTsessionsessionIdname7C2B8DFB": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionrenamesession93520CEA", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/PUT/session/*/name" + ] + ] + } + } + }, + "SessionApiRestApisessionsessionIdnamePUT525B7514": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "PUT", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionrenamesession93520CEA", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "SessionApiRestApisessionsessionIdname2D15D080" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionIdattachImageB793D205": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "SessionApiRestApisessionsessionId2D6ACCD9" + }, + "PathPart": "attachImage", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionIdattachImageOPTIONSBB2E4DC3": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "SessionApiRestApisessionsessionIdattachImageB793D205" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionsessionIdattachImagePUTApiPermissionLisaChatSessionApiRestApiDD07298DPUTsessionsessionIdattachImage81E3E2BD": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionattachimagetosessionA482E30C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/PUT/session/*/attachImage" + ] + ] + } + } + }, + "SessionApiRestApisessionsessionIdattachImagePUTApiPermissionTestLisaChatSessionApiRestApiDD07298DPUTsessionsessionIdattachImage893F6138": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionattachimagetosessionA482E30C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/PUT/session/*/attachImage" + ] + ] + } + } + }, + "SessionApiRestApisessionsessionIdattachImagePUT51934F4F": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "PUT", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "SessionApiLisaChatsessionattachimagetosessionA482E30C", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "SessionApiRestApisessionsessionIdattachImageB793D205" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiRestApisessionDELETEApiPermissionLisaChatSessionApiRestApiDD07298DDELETEsession8799785A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessiondeleteusersessions7AADE5ED", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/DELETE/session" + ] + ] + } + } + }, + "SessionApiRestApisessionDELETEApiPermissionTestLisaChatSessionApiRestApiDD07298DDELETEsession75F06CA1": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SessionApiLisaChatsessiondeleteusersessions7AADE5ED", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/DELETE/session" + ] + ] + } + } + }, + "SessionApiRestApisessionDELETE0DFE30DB": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "DELETE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "SessionApiLisaChatsessiondeleteusersessions7AADE5ED", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "SessionApiRestApisessionADA907DB" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Role used by LISA SessionApi lambdas to access AWS resources", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable", + "dynamodb:GetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "SessionApiSessionsTableDA695141", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "SessionApiSessionsTableDA695141", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "lambdaPermissions" + } + ], + "RoleName": "test-lisa-LisaSessionApiLambdaExecutionRole" + } + }, + "SessionApiLisaSessionApiLambdaExecutionRoleDefaultPolicy8918DE87": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:GetItem", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:dynamodb:us-iso-east-1:012345678901:table/", + { + "Ref": "SsmParameterValuedevtestlisalisamodelTableNameC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + ] + } + }, + { + "Action": [ + "dynamodb:GetItem", + "dynamodb:Query" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ConfigurationApiConfigurationTable4B2B7EE1", + "Arn" + ] + } + }, + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:sqs:us-iso-east-1:012345678901:", + { + "Ref": "SsmParameterValuedevtestlisalisaqueuenameusagemetricsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + ] + } + }, + { + "Action": [ + "kms:GenerateDataKey", + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "SessionApiSessionEncryptionKeyA03622E8", + "Arn" + ] + } + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "SessionApiSessionsTableDA695141", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "SessionApiSessionsTableDA695141", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "GeneratedImagesBucketC3465633", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "GeneratedImagesBucketC3465633", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "SessionApiSessionsTableDA695141", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "SessionApiSessionsTableDA695141", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": "s3:DeleteObject*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "GeneratedImagesBucketC3465633", + "Arn" + ] + }, + "/*" + ] + ] + } + }, + { + "Action": [ + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "SessionApiSessionsTableDA695141", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "SessionApiSessionsTableDA695141", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "GeneratedImagesBucketC3465633", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "GeneratedImagesBucketC3465633", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "SessionApiLisaSessionApiLambdaExecutionRoleDefaultPolicy8918DE87", + "Roles": [ + { + "Ref": "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B" + } + ] + } + }, + "SessionApiLisaChatsessionlistsessionsB88A4A44": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Lists available sessions for user", + "Environment": { + "Variables": { + "SESSIONS_TABLE_NAME": { + "Ref": "SessionApiSessionsTableDA695141" + }, + "SESSIONS_BY_USER_ID_INDEX_NAME": "byUserId", + "GENERATED_IMAGES_S3_BUCKET_NAME": { + "Ref": "GeneratedImagesBucketC3465633" + }, + "MODEL_TABLE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamodelTableNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "CONFIG_TABLE_NAME": { + "Ref": "ConfigurationApiConfigurationTable4B2B7EE1" + }, + "SESSION_ENCRYPTION_KEY_ARN": { + "Fn::GetAtt": [ + "SessionApiSessionEncryptionKeyA03622E8", + "Arn" + ] + }, + "USAGE_METRICS_QUEUE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisaqueuenameusagemetricsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + }, + "FunctionName": "LisaChat-session-list_sessions", + "Handler": "session.lambda_functions.list_sessions", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "SessionApiLisaSessionApiLambdaExecutionRoleDefaultPolicy8918DE87", + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B" + ] + }, + "SessionApiLisaChatsessiongetsessionBC2FF1E4": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Returns the selected session", + "Environment": { + "Variables": { + "SESSIONS_TABLE_NAME": { + "Ref": "SessionApiSessionsTableDA695141" + }, + "SESSIONS_BY_USER_ID_INDEX_NAME": "byUserId", + "GENERATED_IMAGES_S3_BUCKET_NAME": { + "Ref": "GeneratedImagesBucketC3465633" + }, + "MODEL_TABLE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamodelTableNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "CONFIG_TABLE_NAME": { + "Ref": "ConfigurationApiConfigurationTable4B2B7EE1" + }, + "SESSION_ENCRYPTION_KEY_ARN": { + "Fn::GetAtt": [ + "SessionApiSessionEncryptionKeyA03622E8", + "Arn" + ] + }, + "USAGE_METRICS_QUEUE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisaqueuenameusagemetricsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + }, + "FunctionName": "LisaChat-session-get_session", + "Handler": "session.lambda_functions.get_session", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "SessionApiLisaSessionApiLambdaExecutionRoleDefaultPolicy8918DE87", + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B" + ] + }, + "SessionApiLisaChatsessiondeletesession88A67D12": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Deletes selected session", + "Environment": { + "Variables": { + "SESSIONS_TABLE_NAME": { + "Ref": "SessionApiSessionsTableDA695141" + }, + "SESSIONS_BY_USER_ID_INDEX_NAME": "byUserId", + "GENERATED_IMAGES_S3_BUCKET_NAME": { + "Ref": "GeneratedImagesBucketC3465633" + }, + "MODEL_TABLE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamodelTableNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "CONFIG_TABLE_NAME": { + "Ref": "ConfigurationApiConfigurationTable4B2B7EE1" + }, + "SESSION_ENCRYPTION_KEY_ARN": { + "Fn::GetAtt": [ + "SessionApiSessionEncryptionKeyA03622E8", + "Arn" + ] + }, + "USAGE_METRICS_QUEUE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisaqueuenameusagemetricsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + }, + "FunctionName": "LisaChat-session-delete_session", + "Handler": "session.lambda_functions.delete_session", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "SessionApiLisaSessionApiLambdaExecutionRoleDefaultPolicy8918DE87", + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B" + ] + }, + "SessionApiLisaChatsessiondeleteusersessions7AADE5ED": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Deletes all sessions for selected user", + "Environment": { + "Variables": { + "SESSIONS_TABLE_NAME": { + "Ref": "SessionApiSessionsTableDA695141" + }, + "SESSIONS_BY_USER_ID_INDEX_NAME": "byUserId", + "GENERATED_IMAGES_S3_BUCKET_NAME": { + "Ref": "GeneratedImagesBucketC3465633" + }, + "MODEL_TABLE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamodelTableNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "CONFIG_TABLE_NAME": { + "Ref": "ConfigurationApiConfigurationTable4B2B7EE1" + }, + "SESSION_ENCRYPTION_KEY_ARN": { + "Fn::GetAtt": [ + "SessionApiSessionEncryptionKeyA03622E8", + "Arn" + ] + }, + "USAGE_METRICS_QUEUE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisaqueuenameusagemetricsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + }, + "FunctionName": "LisaChat-session-delete_user_sessions", + "Handler": "session.lambda_functions.delete_user_sessions", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "SessionApiLisaSessionApiLambdaExecutionRoleDefaultPolicy8918DE87", + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B" + ] + }, + "SessionApiLisaChatsessionputsession28059E77": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Creates or updates selected session", + "Environment": { + "Variables": { + "SESSIONS_TABLE_NAME": { + "Ref": "SessionApiSessionsTableDA695141" + }, + "SESSIONS_BY_USER_ID_INDEX_NAME": "byUserId", + "GENERATED_IMAGES_S3_BUCKET_NAME": { + "Ref": "GeneratedImagesBucketC3465633" + }, + "MODEL_TABLE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamodelTableNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "CONFIG_TABLE_NAME": { + "Ref": "ConfigurationApiConfigurationTable4B2B7EE1" + }, + "SESSION_ENCRYPTION_KEY_ARN": { + "Fn::GetAtt": [ + "SessionApiSessionEncryptionKeyA03622E8", + "Arn" + ] + }, + "USAGE_METRICS_QUEUE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisaqueuenameusagemetricsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + }, + "FunctionName": "LisaChat-session-put_session", + "Handler": "session.lambda_functions.put_session", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "SessionApiLisaSessionApiLambdaExecutionRoleDefaultPolicy8918DE87", + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B" + ] + }, + "SessionApiLisaChatsessionrenamesession93520CEA": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Updates session name", + "Environment": { + "Variables": { + "SESSIONS_TABLE_NAME": { + "Ref": "SessionApiSessionsTableDA695141" + }, + "SESSIONS_BY_USER_ID_INDEX_NAME": "byUserId", + "GENERATED_IMAGES_S3_BUCKET_NAME": { + "Ref": "GeneratedImagesBucketC3465633" + }, + "MODEL_TABLE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamodelTableNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "CONFIG_TABLE_NAME": { + "Ref": "ConfigurationApiConfigurationTable4B2B7EE1" + }, + "SESSION_ENCRYPTION_KEY_ARN": { + "Fn::GetAtt": [ + "SessionApiSessionEncryptionKeyA03622E8", + "Arn" + ] + }, + "USAGE_METRICS_QUEUE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisaqueuenameusagemetricsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + }, + "FunctionName": "LisaChat-session-rename_session", + "Handler": "session.lambda_functions.rename_session", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "SessionApiLisaSessionApiLambdaExecutionRoleDefaultPolicy8918DE87", + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B" + ] + }, + "SessionApiLisaChatsessionattachimagetosessionA482E30C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Attaches image to session", + "Environment": { + "Variables": { + "SESSIONS_TABLE_NAME": { + "Ref": "SessionApiSessionsTableDA695141" + }, + "SESSIONS_BY_USER_ID_INDEX_NAME": "byUserId", + "GENERATED_IMAGES_S3_BUCKET_NAME": { + "Ref": "GeneratedImagesBucketC3465633" + }, + "MODEL_TABLE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamodelTableNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "CONFIG_TABLE_NAME": { + "Ref": "ConfigurationApiConfigurationTable4B2B7EE1" + }, + "SESSION_ENCRYPTION_KEY_ARN": { + "Fn::GetAtt": [ + "SessionApiSessionEncryptionKeyA03622E8", + "Arn" + ] + }, + "USAGE_METRICS_QUEUE_NAME": { + "Ref": "SsmParameterValuedevtestlisalisaqueuenameusagemetricsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + } + }, + "FunctionName": "LisaChat-session-attach_image_to_session", + "Handler": "session.lambda_functions.attach_image_to_session", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "SessionApiLisaSessionApiLambdaExecutionRoleDefaultPolicy8918DE87", + "SessionApiLisaSessionApiLambdaExecutionRoleC453EA6B" + ] + }, + "GeneratedImagesBucketC3465633": { + "Type": "AWS::S3::Bucket", + "Properties": { + "CorsConfiguration": { + "CorsRules": [ + { + "AllowedHeaders": [ + "*" + ], + "AllowedMethods": [ + "GET", + "POST" + ], + "AllowedOrigins": [ + "*" + ], + "ExposedHeaders": [ + "Access-Control-Allow-Origin" + ] + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + }, + "LogFilePrefix": "logs/generated-images-bucket/" + }, + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "GeneratedImagesBucketPolicyA72A6FC9": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "GeneratedImagesBucketC3465633" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "GeneratedImagesBucketC3465633", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "GeneratedImagesBucketC3465633", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:PutBucketPolicy", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "GeneratedImagesBucketC3465633", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "GeneratedImagesBucketC3465633", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "GeneratedImagesBucketAutoDeleteObjectsCustomResource907225F0": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "GeneratedImagesBucketC3465633" + } + }, + "DependsOn": [ + "GeneratedImagesBucketPolicyA72A6FC9" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": "nodejs22.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "GeneratedImagesBucketC3465633" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "PromptTemplateApiPromptTemplatesTable2B59FA4A": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + }, + { + "AttributeName": "created", + "AttributeType": "S" + }, + { + "AttributeName": "owner", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "GlobalSecondaryIndexes": [ + { + "IndexName": "byOwner", + "KeySchema": [ + { + "AttributeName": "owner", + "KeyType": "HASH" + }, + { + "AttributeName": "created", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "byLatest", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + }, + { + "AttributeName": "created", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + }, + { + "AttributeName": "created", + "KeyType": "RANGE" + } + ], + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Role used by LISA PromptTemplatesApi lambdas to access AWS resources", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable", + "dynamodb:GetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PromptTemplateApiPromptTemplatesTable2B59FA4A", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PromptTemplateApiPromptTemplatesTable2B59FA4A", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "lambdaPermissions" + } + ], + "RoleName": "test-lisa-LisaPromptTemplatesApiLambdaExecutionRole" + } + }, + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRoleDefaultPolicyF1F54E4F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PromptTemplateApiPromptTemplatesTable2B59FA4A", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PromptTemplateApiPromptTemplatesTable2B59FA4A", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PromptTemplateApiPromptTemplatesTable2B59FA4A", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PromptTemplateApiPromptTemplatesTable2B59FA4A", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PromptTemplateApiPromptTemplatesTable2B59FA4A", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PromptTemplateApiPromptTemplatesTable2B59FA4A", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRoleDefaultPolicyF1F54E4F", + "Roles": [ + { + "Ref": "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E" + } + ] + } + }, + "PromptTemplateApiRestApiprompttemplates78FD216B": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "prompt-templates", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "PromptTemplateApiRestApiprompttemplatesOPTIONSF9B1B2D1": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "PromptTemplateApiRestApiprompttemplates78FD216B" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "PromptTemplateApiRestApiprompttemplatesPOSTApiPermissionLisaChatPromptTemplateApiRestApiF7D4C874POSTprompttemplates17A56F2E": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatescreateC453FE00", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/POST/prompt-templates" + ] + ] + } + } + }, + "PromptTemplateApiRestApiprompttemplatesPOSTApiPermissionTestLisaChatPromptTemplateApiRestApiF7D4C874POSTprompttemplatesB52CD905": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatescreateC453FE00", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/POST/prompt-templates" + ] + ] + } + } + }, + "PromptTemplateApiRestApiprompttemplatesPOST30586757": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "POST", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatescreateC453FE00", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "PromptTemplateApiRestApiprompttemplates78FD216B" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateId0A2BE223": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "PromptTemplateApiRestApiprompttemplates78FD216B" + }, + "PathPart": "{promptTemplateId}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateIdOPTIONSD02AC196": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "PromptTemplateApiRestApiprompttemplatespromptTemplateId0A2BE223" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateIdGETApiPermissionLisaChatPromptTemplateApiRestApiF7D4C874GETprompttemplatespromptTemplateIdA936EC7C": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatesgetA1792182", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/prompt-templates/*" + ] + ] + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateIdGETApiPermissionTestLisaChatPromptTemplateApiRestApiF7D4C874GETprompttemplatespromptTemplateId4FE0C8DE": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatesgetA1792182", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/prompt-templates/*" + ] + ] + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateIdGETC3F57ABA": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatesgetA1792182", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "PromptTemplateApiRestApiprompttemplatespromptTemplateId0A2BE223" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateIdPUTApiPermissionLisaChatPromptTemplateApiRestApiF7D4C874PUTprompttemplatespromptTemplateId03327213": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatesupdateDA9BD670", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/PUT/prompt-templates/*" + ] + ] + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateIdPUTApiPermissionTestLisaChatPromptTemplateApiRestApiF7D4C874PUTprompttemplatespromptTemplateId95AE4205": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatesupdateDA9BD670", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/PUT/prompt-templates/*" + ] + ] + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateIdPUTCFAC9871": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "PUT", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatesupdateDA9BD670", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "PromptTemplateApiRestApiprompttemplatespromptTemplateId0A2BE223" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateIdDELETEApiPermissionLisaChatPromptTemplateApiRestApiF7D4C874DELETEprompttemplatespromptTemplateIdC2A4EFEE": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatesdelete41FB2B92", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/DELETE/prompt-templates/*" + ] + ] + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateIdDELETEApiPermissionTestLisaChatPromptTemplateApiRestApiF7D4C874DELETEprompttemplatespromptTemplateIdD401F4D8": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatesdelete41FB2B92", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/DELETE/prompt-templates/*" + ] + ] + } + } + }, + "PromptTemplateApiRestApiprompttemplatespromptTemplateIdDELETE46CD0D26": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "DELETE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplatesdelete41FB2B92", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "PromptTemplateApiRestApiprompttemplatespromptTemplateId0A2BE223" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "PromptTemplateApiRestApiprompttemplatesGETApiPermissionLisaChatPromptTemplateApiRestApiF7D4C874GETprompttemplates2250C01D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplateslist70C16F55", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/prompt-templates" + ] + ] + } + } + }, + "PromptTemplateApiRestApiprompttemplatesGETApiPermissionTestLisaChatPromptTemplateApiRestApiF7D4C874GETprompttemplatesC0A0C088": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplateslist70C16F55", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/prompt-templates" + ] + ] + } + } + }, + "PromptTemplateApiRestApiprompttemplatesGETB12AB0B0": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "PromptTemplateApiLisaChatprompttemplateslist70C16F55", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "PromptTemplateApiRestApiprompttemplates78FD216B" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "PromptTemplateApiLisaChatprompttemplatescreateC453FE00": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Creates prompt template", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "PROMPT_TEMPLATES_TABLE_NAME": { + "Ref": "PromptTemplateApiPromptTemplatesTable2B59FA4A" + }, + "PROMPT_TEMPLATES_BY_LATEST_INDEX_NAME": "byOwner" + } + }, + "FunctionName": "LisaChat-prompt_templates-create", + "Handler": "prompt_templates.lambda_functions.create", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRoleDefaultPolicyF1F54E4F", + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E" + ] + }, + "PromptTemplateApiLisaChatprompttemplatesgetA1792182": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Retrieves specific prompt template by ID", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "PROMPT_TEMPLATES_TABLE_NAME": { + "Ref": "PromptTemplateApiPromptTemplatesTable2B59FA4A" + }, + "PROMPT_TEMPLATES_BY_LATEST_INDEX_NAME": "byOwner" + } + }, + "FunctionName": "LisaChat-prompt_templates-get", + "Handler": "prompt_templates.lambda_functions.get", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRoleDefaultPolicyF1F54E4F", + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E" + ] + }, + "PromptTemplateApiLisaChatprompttemplateslist70C16F55": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Lists all available prompt templates", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "PROMPT_TEMPLATES_TABLE_NAME": { + "Ref": "PromptTemplateApiPromptTemplatesTable2B59FA4A" + }, + "PROMPT_TEMPLATES_BY_LATEST_INDEX_NAME": "byOwner" + } + }, + "FunctionName": "LisaChat-prompt_templates-list", + "Handler": "prompt_templates.lambda_functions.list", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRoleDefaultPolicyF1F54E4F", + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E" + ] + }, + "PromptTemplateApiLisaChatprompttemplatesupdateDA9BD670": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Updates an existing prompt template", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "PROMPT_TEMPLATES_TABLE_NAME": { + "Ref": "PromptTemplateApiPromptTemplatesTable2B59FA4A" + }, + "PROMPT_TEMPLATES_BY_LATEST_INDEX_NAME": "byOwner" + } + }, + "FunctionName": "LisaChat-prompt_templates-update", + "Handler": "prompt_templates.lambda_functions.update", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRoleDefaultPolicyF1F54E4F", + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E" + ] + }, + "PromptTemplateApiLisaChatprompttemplatesdelete41FB2B92": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Deletes a specific prompt template by ID", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "PROMPT_TEMPLATES_TABLE_NAME": { + "Ref": "PromptTemplateApiPromptTemplatesTable2B59FA4A" + }, + "PROMPT_TEMPLATES_BY_LATEST_INDEX_NAME": "byOwner" + } + }, + "FunctionName": "LisaChat-prompt_templates-delete", + "Handler": "prompt_templates.lambda_functions.delete", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRoleDefaultPolicyF1F54E4F", + "PromptTemplateApiLisaPromptTemplatesApiLambdaExecutionRole941C178E" + ] + }, + "UserPreferencesApiUserPreferencesTableD7C804C6": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "user", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "user", + "KeyType": "HASH" + } + ], + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "UserPreferencesApiRestApiuserpreferencesECCFBF5F": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "user-preferences", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "UserPreferencesApiRestApiuserpreferencesOPTIONS5396137B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "UserPreferencesApiRestApiuserpreferencesECCFBF5F" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "UserPreferencesApiRestApiuserpreferencesGETApiPermissionLisaChatUserPreferencesApiRestApi1A0B6696GETuserpreferencesD3DF88F8": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "UserPreferencesApiLisaChatuserpreferencesgetCAACC747", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/user-preferences" + ] + ] + } + } + }, + "UserPreferencesApiRestApiuserpreferencesGETApiPermissionTestLisaChatUserPreferencesApiRestApi1A0B6696GETuserpreferences3D302555": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "UserPreferencesApiLisaChatuserpreferencesgetCAACC747", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/user-preferences" + ] + ] + } + } + }, + "UserPreferencesApiRestApiuserpreferencesGET30F3E592": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "UserPreferencesApiLisaChatuserpreferencesgetCAACC747", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "UserPreferencesApiRestApiuserpreferencesECCFBF5F" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "UserPreferencesApiRestApiuserpreferencesPUTApiPermissionLisaChatUserPreferencesApiRestApi1A0B6696PUTuserpreferences75727D9C": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "UserPreferencesApiLisaChatuserpreferencesupdateC679459F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/PUT/user-preferences" + ] + ] + } + } + }, + "UserPreferencesApiRestApiuserpreferencesPUTApiPermissionTestLisaChatUserPreferencesApiRestApi1A0B6696PUTuserpreferences6193E000": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "UserPreferencesApiLisaChatuserpreferencesupdateC679459F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/PUT/user-preferences" + ] + ] + } + } + }, + "UserPreferencesApiRestApiuserpreferencesPUT19B326BE": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "PUT", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "UserPreferencesApiLisaChatuserpreferencesupdateC679459F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "UserPreferencesApiRestApiuserpreferencesECCFBF5F" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "UserPreferencesApiLisaUserPreferencesApiLambdaExecutionRole44353F29": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Role used by LISA UserPreferencesApi lambdas to access AWS resources", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable", + "dynamodb:GetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "UserPreferencesApiUserPreferencesTableD7C804C6", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "UserPreferencesApiUserPreferencesTableD7C804C6", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "lambdaPermissions" + } + ], + "RoleName": "test-lisa-LisaUserPreferencesApiLambdaExecutionRole" + } + }, + "UserPreferencesApiLisaUserPreferencesApiLambdaExecutionRoleDefaultPolicyA4B6F391": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "UserPreferencesApiUserPreferencesTableD7C804C6", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Action": [ + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "UserPreferencesApiUserPreferencesTableD7C804C6", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "UserPreferencesApiLisaUserPreferencesApiLambdaExecutionRoleDefaultPolicyA4B6F391", + "Roles": [ + { + "Ref": "UserPreferencesApiLisaUserPreferencesApiLambdaExecutionRole44353F29" + } + ] + } + }, + "UserPreferencesApiLisaChatuserpreferencesgetCAACC747": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Returns the preferences for the calling user", + "Environment": { + "Variables": { + "USER_PREFERENCES_TABLE_NAME": { + "Ref": "UserPreferencesApiUserPreferencesTableD7C804C6" + } + } + }, + "FunctionName": "LisaChat-user_preferences-get", + "Handler": "user_preferences.lambda_functions.get", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "UserPreferencesApiLisaUserPreferencesApiLambdaExecutionRole44353F29", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "UserPreferencesApiLisaUserPreferencesApiLambdaExecutionRoleDefaultPolicyA4B6F391", + "UserPreferencesApiLisaUserPreferencesApiLambdaExecutionRole44353F29" + ] + }, + "UserPreferencesApiLisaChatuserpreferencesupdateC679459F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Creates or updates user preferences for user", + "Environment": { + "Variables": { + "USER_PREFERENCES_TABLE_NAME": { + "Ref": "UserPreferencesApiUserPreferencesTableD7C804C6" + } + } + }, + "FunctionName": "LisaChat-user_preferences-update", + "Handler": "user_preferences.lambda_functions.update", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "UserPreferencesApiLisaUserPreferencesApiLambdaExecutionRole44353F29", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "UserPreferencesApiLisaUserPreferencesApiLambdaExecutionRoleDefaultPolicyA4B6F391", + "UserPreferencesApiLisaUserPreferencesApiLambdaExecutionRole44353F29" + ] + } + }, + "Parameters": { + "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/layerVersion/common" + }, + "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/layerVersion/fastapi" + }, + "SsmParameterValuedevtestlisalisaserveendpointC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/serve/endpoint" + }, + "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/bucket/bucket-access-logs" + }, + "SsmParameterValuedevtestlisalisamodelTableNameC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/modelTableName" + }, + "SsmParameterValuedevtestlisalisaqueuenameusagemetricsC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/queue-name/usage-metrics" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "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/test/cdk/stacks/__baselines__/LisaCore.json b/test/cdk/stacks/__baselines__/LisaCore.json new file mode 100644 index 000000000..6828b1961 --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaCore.json @@ -0,0 +1,305 @@ +{ + "Resources": { + "BucketAccessLogsBucket91990836": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "test-lisa-012345678901-dev-bucket-access-logs", + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "BucketAccessLogsBucketPolicyC2F836B4": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "BucketAccessLogsBucket91990836" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "BucketAccessLogsBucket91990836", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketAccessLogsBucket91990836", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:PutBucketPolicy", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "BucketAccessLogsBucket91990836", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketAccessLogsBucket91990836", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "BucketAccessLogsBucketAutoDeleteObjectsCustomResource4A877306": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "BucketAccessLogsBucket91990836" + } + }, + "DependsOn": [ + "BucketAccessLogsBucketPolicyC2F836B4" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": "nodejs22.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "BucketAccessLogsBucket91990836" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "LISABucketAccessLogsBucket693E0ACF": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "A bucket for access logs from other buckets to be written to.", + "Name": "/dev/test-lisa/lisa/bucket/bucket-access-logs", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "BucketAccessLogsBucket91990836", + "Arn" + ] + } + } + }, + "CommonLayer4CE7033F": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "python3.11" + ], + "Content": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "0256ab942aec599d8fa7a5123d18bc0bbd14cdfd15b8e41c6b4fdba22e8ec112.zip" + }, + "Description": "Common requirements for REST API Lambdas" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "FastapiLayer750D565D": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "python3.11" + ], + "Content": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "8169090c8df369ecb39c5edabdb022efacda96947f90a31bbee407832cbf7f00.zip" + }, + "Description": "FastAPI requirements for REST API Lambdas" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AuthorizerLayer295C198D": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "python3.11" + ], + "Content": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d85617b013352d8d4be6a53c62b85ece35379ffae830bae89e048e6f16aabca9.zip" + }, + "Description": "API authorization dependencies for REST API" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LisaCommonLamdaLayerStringParameter39A4663D": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Layer Version ARN for LISA Common Lambda Layer", + "Name": "/dev/test-lisa/lisa/layerVersion/common", + "Type": "String", + "Value": { + "Ref": "CommonLayer4CE7033F" + } + } + }, + "LisaFastapiLamdaLayerStringParameter4D43C161": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Layer Version ARN for LISA FastAPI Lambda Layer", + "Name": "/dev/test-lisa/lisa/layerVersion/fastapi", + "Type": "String", + "Value": { + "Ref": "FastapiLayer750D565D" + } + } + }, + "LisaAuthorizerLamdaLayerStringParameter50611625": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Layer Version ARN for LISA Authorizer Lambda Layer", + "Name": "/dev/test-lisa/lisa/layerVersion/authorizer", + "Type": "String", + "Value": { + "Ref": "AuthorizerLayer295C198D" + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "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/test/cdk/stacks/__baselines__/LisaDocs.json b/test/cdk/stacks/__baselines__/LisaDocs.json new file mode 100644 index 000000000..70fa35e44 --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaDocs.json @@ -0,0 +1,811 @@ +{ + "Parameters": { + "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/bucket/bucket-access-logs" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Resources": { + "DocsBucketECEA003F": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + }, + "LogFilePrefix": "logs/docs-bucket/" + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + }, + { + "Key": "aws-cdk:cr-owned:a8bbbea7", + "Value": "true" + } + ], + "WebsiteConfiguration": { + "ErrorDocument": "404.html", + "IndexDocument": "index.html" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DocsBucketPolicy7BCF1FB7": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "DocsBucketECEA003F" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "DocsBucketECEA003F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "DocsBucketECEA003F", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:PutBucketPolicy", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "DocsBucketECEA003F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "DocsBucketECEA003F", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "DocsBucketAutoDeleteObjectsCustomResourceADA97380": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "DocsBucketECEA003F" + } + }, + "DependsOn": [ + "DocsBucketPolicy7BCF1FB7" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": "nodejs22.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "DocsBucketECEA003F" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "DeployDocsWebsiteAwsCliLayerB8E45E5B": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "c49d356cac773d491c5f7ac148995a1181498a8e289429f8612a7f7e3814f535.zip" + }, + "Description": "/opt/awscli/aws" + } + }, + "DeployDocsWebsiteCustomResourceA9351820": { + "Type": "Custom::CDKBucketDeployment", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", + "Arn" + ] + }, + "SourceBucketNames": [ + "cdk-hnb659fds-assets-012345678901-us-iso-east-1" + ], + "SourceObjectKeys": [ + "8b817ef2dc7053bfbb0a9fd32dfe7d8b8047180bcc7f34c1c2b16e93f8cbe56d.zip" + ], + "DestinationBucketName": { + "Ref": "DocsBucketECEA003F" + }, + "WaitForDistributionInvalidation": true, + "Prune": true, + "OutputObjectKeys": true + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": { + "Type": "AWS::IAM::Role", + "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" + ] + ] + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::cdk-hnb659fds-assets-012345678901-us-iso-east-1" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::cdk-hnb659fds-assets-012345678901-us-iso-east-1/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "DocsBucketECEA003F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "DocsBucketECEA003F", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "Roles": [ + { + "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9.zip" + }, + "Environment": { + "Variables": { + "AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "DeployDocsWebsiteAwsCliLayerB8E45E5B" + } + ], + "Role": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + "Arn" + ] + }, + "Runtime": "python3.13", + "Timeout": 900 + }, + "DependsOn": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + ] + }, + "LisaDocss3readerrole85268898": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Allows API gateway to proxy static website assets" + } + }, + "LisaDocss3readerroleDefaultPolicyEF51CA28": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "DocsBucketECEA003F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "DocsBucketECEA003F", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LisaDocss3readerroleDefaultPolicyEF51CA28", + "Roles": [ + { + "Ref": "LisaDocss3readerrole85268898" + } + ] + } + }, + "DocsApi5763FC77": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "BinaryMediaTypes": [ + "*/*" + ], + "Description": "API Gateway for S3 hosted website", + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Name": "DocsApi" + } + }, + "DocsApiCloudWatchRole725D7C7C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "DocsApiAccount902647F9": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "DocsApiCloudWatchRole725D7C7C", + "Arn" + ] + } + }, + "DependsOn": [ + "DocsApi5763FC77" + ], + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "DocsApiDeploymentE064540Dc7c75e9a194700942ffb536a75897cc3": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "API Gateway for S3 hosted website", + "RestApiId": { + "Ref": "DocsApi5763FC77" + } + }, + "DependsOn": [ + "DocsApikeyGETC28AE243", + "DocsApikey6C86E256", + "DocsApiGETC1B4927F" + ], + "Metadata": { + "aws:cdk:do-not-refactor": true + } + }, + "DocsApiDeploymentStageLISA766658E9": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "DocsApiDeploymentE064540Dc7c75e9a194700942ffb536a75897cc3" + }, + "RestApiId": { + "Ref": "DocsApi5763FC77" + }, + "StageName": "LISA" + }, + "DependsOn": [ + "DocsApiAccount902647F9" + ] + }, + "DocsApiGETC1B4927F": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "NONE", + "HttpMethod": "GET", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "LisaDocss3readerrole85268898", + "Arn" + ] + }, + "IntegrationHttpMethod": "GET", + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": "'text/html'" + }, + "StatusCode": "200" + } + ], + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:s3:path/", + { + "Ref": "DocsBucketECEA003F" + }, + "/index.html" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + } + ], + "ResourceId": { + "Fn::GetAtt": [ + "DocsApi5763FC77", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "DocsApi5763FC77" + } + } + }, + "DocsApikey6C86E256": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "DocsApi5763FC77", + "RootResourceId" + ] + }, + "PathPart": "{key+}", + "RestApiId": { + "Ref": "DocsApi5763FC77" + } + } + }, + "DocsApikeyGETC28AE243": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "NONE", + "HttpMethod": "GET", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "LisaDocss3readerrole85268898", + "Arn" + ] + }, + "IntegrationHttpMethod": "GET", + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": "integration.response.header.Content-Type", + "method.response.header.Content-Disposition": "integration.response.header.Content-Disposition", + "method.response.header.Content-Length": "integration.response.header.Content-Length" + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": "'text/html'" + }, + "ResponseTemplates": { + "text/html": "#set($context.responseOverride.header.Content-Type = 'text/html')\n #set($context.responseOverride.status = 404)\n #set($context.responseOverride.header.Location = \"$context.domainName/404.html\")" + }, + "SelectionPattern": "403", + "StatusCode": "404" + } + ], + "RequestParameters": { + "integration.request.path.key": "method.request.path.key" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:s3:path/", + { + "Ref": "DocsBucketECEA003F" + }, + "/{key}" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true, + "method.response.header.Content-Disposition": true, + "method.response.header.Content-Length": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "404" + } + ], + "RequestParameters": { + "method.request.path.key": true + }, + "ResourceId": { + "Ref": "DocsApikey6C86E256" + }, + "RestApiId": { + "Ref": "DocsApi5763FC77" + } + } + } + }, + "Outputs": { + "DocsApiEndpointD6E3ED14": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "DocsApi5763FC77" + }, + ".execute-api.us-iso-east-1.", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "DocsApiDeploymentStageLISA766658E9" + }, + "/" + ] + ] + } + }, + "DocsApiGatewayUrl": { + "Description": "API Gateway URL", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "DocsApi5763FC77" + }, + ".execute-api.us-iso-east-1.", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "DocsApiDeploymentStageLISA766658E9" + }, + "/" + ] + ] + } + } + }, + "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/test/cdk/stacks/__baselines__/LisaIAM.json b/test/cdk/stacks/__baselines__/LisaIAM.json new file mode 100644 index 000000000..efce9c4af --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaIAM.json @@ -0,0 +1,808 @@ +{ + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Allow use of AmazonSSMManagedInstanceCore policy to allow EC2 to enable SSM core functionality.", + "id": "AwsSolutions-IAM4" + } + ] + } + }, + "Resources": { + "testlisaRAGPolicyD60E2774": { + "Type": "AWS::IAM::ManagedPolicy", + "Properties": { + "Description": "", + "ManagedPolicyName": "test-lisa-RAGPolicy", + "Path": "/", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeSubnets", + "ec2:DeleteNetworkInterface", + "ec2:AssignPrivateIpAddresses", + "ec2:UnassignPrivateIpAddresses" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "secretsmanager:GetSecretValue", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*/*/POST/serve/embeddings" + ] + ] + } + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/*" + ] + ] + } + }, + { + "Action": "iam:GetServerCertificate", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":server-certificate/*" + ] + ] + } + }, + { + "Action": [ + "s3:PutObject", + "s3:DeleteObject" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "bedrock:StartIngestionJob", + "bedrock:Retrieve" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + } + }, + "LisaRagLambdaExecutionRole9186056F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Role used by RAG API lambdas to access AWS resources", + "ManagedPolicyArns": [ + { + "Ref": "testlisaRAGPolicyD60E2774" + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + ] + ] + } + ], + "RoleName": "test-lisa-LisaRagLambdaExecutionRole" + } + }, + "LisaRagRoleStringParameterB28A4A29": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Role ARN for LISA test-lisa-LisaRagLambdaExecutionRole", + "Name": "/dev/test-lisa/lisa/roles/test-lisa-LisaRagLambdaExecutionRole", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "LisaRagLambdaExecutionRole9186056F", + "Arn" + ] + } + } + }, + "testlisaECSPolicy23A3F2E3": { + "Type": "AWS::IAM::ManagedPolicy", + "Properties": { + "Description": "", + "ManagedPolicyName": "test-lisa-ECSPolicy", + "Path": "/", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cloudformation:DescribeStacks", + "cloudformation:DescribeStackResources", + "cloudformation:GetTemplate" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "autoscaling:DescribeAutoScalingGroups", + "autoscaling:DescribeAutoScalingInstances", + "autoscaling:DescribeLoadBalancers", + "autoscaling:DescribeNotificationConfigurations", + "autoscaling:DescribePolicies", + "autoscaling:DescribeScalingActivities" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "application-autoscaling:CreateScalingPolicy", + "application-autoscaling:DeleteScalingPolicy", + "application-autoscaling:DeregisterScalableTarget", + "application-autoscaling:PutScalingPolicy", + "application-autoscaling:RegisterScalableTarget" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":application-autoscaling:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":scalable-target/*" + ] + ] + } + }, + { + "Action": "application-autoscaling:DescribeScalableTargets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "cloudwatch:DescribeAlarms", + "cloudwatch:PutMetricData", + "cloudwatch:GetMetricData", + "cloudwatch:GetMetricStatistics", + "cloudwatch:ListMetrics" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "cloudwatch:PutMetricAlarm", + "cloudwatch:DeleteAlarms" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudwatch:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":alarm:*" + ] + ] + } + }, + { + "Action": [ + "ec2:DescribeInstances", + "ec2:DescribeInstanceAttribute", + "ec2:DescribeInstanceTypes", + "ec2:DescribeImages", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ec2:CreateSecurityGroup", + "ec2:DeleteSecurityGroup", + "ec2:AuthorizeSecurityGroupIngress", + "ec2:AuthorizeSecurityGroupEgress", + "ec2:RevokeSecurityGroupIngress", + "ec2:RevokeSecurityGroupEgress" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":security-group/*" + ] + ] + } + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:DescribeLogGroups", + "logs:CreateLogStream", + "logs:FilterLogEvents", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + }, + { + "Action": [ + "ecs:CreateCluster", + "ecs:DescribeClusters", + "ecs:DescribeContainerInstances", + "ecs:DescribeServices", + "ecs:DescribeTaskDefinition", + "ecs:DescribeTasks", + "ecs:ListClusters", + "ecs:ListServices", + "ecs:ListTaskDefinitions", + "ecs:RegisterTaskDefinition" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:RunTask", + "ecs:StartTask", + "ecs:StopTask" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":task-definition/*" + ] + ] + } + }, + { + "Action": [ + "ecs:CreateService", + "ecs:UpdateService" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":service/*" + ] + ] + } + }, + { + "Action": "ecs:SubmitContainerStateChange", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":cluster/*" + ] + ] + } + }, + { + "Action": [ + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:DescribeTargetHealth" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "events:PutEvents", + "events:DescribeRule" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "ecs-tasks.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "ec2.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::*:role/ecsInstanceRole*" + ] + ] + } + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "application-autoscaling.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::*:role/ecsAutoscaleRole*" + ] + ] + } + }, + { + "Action": "iam:CreateServiceLinkedRole", + "Condition": { + "StringLike": { + "iam:AWSServiceName": [ + "autoscaling.amazonaws.com", + "ecs.amazonaws.com", + "ecs.application-autoscaling.amazonaws.com" + ] + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "secretsmanager:GetSecretValue", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:ListBucket" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::*-mcpworkbench-*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::*-mcpworkbench-*/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "ECSPolicySP94081473": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Managed Policy ARN for LISA test-lisa-ECSPolicy", + "Name": "/dev/test-lisa/lisa/policies/test-lisa-ECSPolicy", + "Type": "String", + "Value": { + "Ref": "testlisaECSPolicy23A3F2E3" + } + } + }, + "RESTRole7934C79A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Allow REST API task access to AWS resources", + "ManagedPolicyArns": [ + { + "Ref": "testlisaECSPolicy23A3F2E3" + } + ], + "RoleName": "test-lisa-REST-Role" + } + }, + "testlisaRESTSPB08D7F90": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Role ARN for LISA API REST ECS Task", + "Name": "/dev/test-lisa/lisa/roles/REST", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "RESTRole7934C79A", + "Arn" + ] + } + } + }, + "RESTExRole5D4D16C7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Allow REST API execution role to pull images and write logs", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + ] + ] + } + ], + "RoleName": "test-lisa-REST-ExRole" + } + }, + "testlisaRESTEXSPD1152CF6": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Role ARN for LISA API REST ECS Execution", + "Name": "/dev/test-lisa/lisa/roles/RESTEX", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "RESTExRole5D4D16C7", + "Arn" + ] + } + } + }, + "MCPWORKBENCHRoleFF2E36E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Allow MCPWORKBENCH API task access to AWS resources", + "ManagedPolicyArns": [ + { + "Ref": "testlisaECSPolicy23A3F2E3" + } + ], + "RoleName": "test-lisa-MCPWORKBENCH-Role" + } + }, + "testlisaMCPWORKBENCHSP9C2285DD": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Role ARN for LISA API MCPWORKBENCH ECS Task", + "Name": "/dev/test-lisa/lisa/roles/MCPWORKBENCH", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "MCPWORKBENCHRoleFF2E36E5", + "Arn" + ] + } + } + }, + "MCPWORKBENCHExRoleA8E5F343": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Allow MCPWORKBENCH API execution role to pull images and write logs", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + ] + ] + } + ], + "RoleName": "test-lisa-MCPWORKBENCH-ExRole" + } + }, + "testlisaMCPWORKBENCHEXSP3D41C369": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Role ARN for LISA API MCPWORKBENCH ECS Execution", + "Name": "/dev/test-lisa/lisa/roles/MCPWORKBENCHEX", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "MCPWORKBENCHExRoleA8E5F343", + "Arn" + ] + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "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/test/cdk/stacks/__baselines__/LisaMcpWorkbench.json b/test/cdk/stacks/__baselines__/LisaMcpWorkbench.json new file mode 100644 index 000000000..a22fd9fa7 --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaMcpWorkbench.json @@ -0,0 +1,1744 @@ +{ + "Resources": { + "McpWorkbenchRestApimcpworkbench4F4E4F8B": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "mcp-workbench", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchOPTIONSC69E0396": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "McpWorkbenchRestApimcpworkbench4F4E4F8B" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchGETApiPermissionLisaMcpWorkbenchRestApi41336B6BGETmcpworkbenchA1A55A1B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchlist046D8BF5", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/mcp-workbench" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchGETApiPermissionTestLisaMcpWorkbenchRestApi41336B6BGETmcpworkbenchF6A82A34": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchlist046D8BF5", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/mcp-workbench" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchGET17EB9312": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchlist046D8BF5", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpWorkbenchRestApimcpworkbench4F4E4F8B" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchPOSTApiPermissionLisaMcpWorkbenchRestApi41336B6BPOSTmcpworkbenchF1A7D4EE": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchcreateE9F26E3A", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/POST/mcp-workbench" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchPOSTApiPermissionTestLisaMcpWorkbenchRestApi41336B6BPOSTmcpworkbench772B04E7": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchcreateE9F26E3A", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/POST/mcp-workbench" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchPOST6F3D2A07": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "POST", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchcreateE9F26E3A", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpWorkbenchRestApimcpworkbench4F4E4F8B" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolId50327D76": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "McpWorkbenchRestApimcpworkbench4F4E4F8B" + }, + "PathPart": "{toolId}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolIdOPTIONS951BD49F": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "McpWorkbenchRestApimcpworkbenchtoolId50327D76" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolIdGETApiPermissionLisaMcpWorkbenchRestApi41336B6BGETmcpworkbenchtoolId93C2F631": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchread7EECA39E", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/mcp-workbench/*" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolIdGETApiPermissionTestLisaMcpWorkbenchRestApi41336B6BGETmcpworkbenchtoolIdE65A551A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchread7EECA39E", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/mcp-workbench/*" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolIdGETDBDBE96E": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchread7EECA39E", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpWorkbenchRestApimcpworkbenchtoolId50327D76" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolIdPUTApiPermissionLisaMcpWorkbenchRestApi41336B6BPUTmcpworkbenchtoolId956969F8": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchupdate830E4B0B", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/PUT/mcp-workbench/*" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolIdPUTApiPermissionTestLisaMcpWorkbenchRestApi41336B6BPUTmcpworkbenchtoolId9C1FC100": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchupdate830E4B0B", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/PUT/mcp-workbench/*" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolIdPUTBBE3BEAC": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "PUT", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchupdate830E4B0B", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpWorkbenchRestApimcpworkbenchtoolId50327D76" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolIdDELETEApiPermissionLisaMcpWorkbenchRestApi41336B6BDELETEmcpworkbenchtoolId9E01534A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchdelete020452F9", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/DELETE/mcp-workbench/*" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolIdDELETEApiPermissionTestLisaMcpWorkbenchRestApi41336B6BDELETEmcpworkbenchtoolId1C21E506": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchdelete020452F9", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/DELETE/mcp-workbench/*" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchtoolIdDELETED4D69E25": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "DELETE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchdelete020452F9", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpWorkbenchRestApimcpworkbenchtoolId50327D76" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchvalidatesyntaxAA8355CF": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "McpWorkbenchRestApimcpworkbench4F4E4F8B" + }, + "PathPart": "validate-syntax", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchvalidatesyntaxOPTIONSDB62E119": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "McpWorkbenchRestApimcpworkbenchvalidatesyntaxAA8355CF" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchRestApimcpworkbenchvalidatesyntaxPOSTApiPermissionLisaMcpWorkbenchRestApi41336B6BPOSTmcpworkbenchvalidatesyntax10E5C787": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchvalidatesyntax4307220C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/POST/mcp-workbench/validate-syntax" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchvalidatesyntaxPOSTApiPermissionTestLisaMcpWorkbenchRestApi41336B6BPOSTmcpworkbenchvalidatesyntax0D7A7C4A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchvalidatesyntax4307220C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/POST/mcp-workbench/validate-syntax" + ] + ] + } + } + }, + "McpWorkbenchRestApimcpworkbenchvalidatesyntaxPOSTD3BEA3CB": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "POST", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchvalidatesyntax4307220C", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "McpWorkbenchRestApimcpworkbenchvalidatesyntaxAA8355CF" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "McpWorkbenchLambdaExecutionRole43E4060B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "IAM role for Lambda function execution", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EC2NetworkInterfaces" + } + ] + } + }, + "McpWorkbenchLambdaExecutionRoleDefaultPolicyAB1DECE8": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "LISAMCPWorkbenchtestlisadevC221720C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISAMCPWorkbenchtestlisadevC221720C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "LISAMCPWorkbenchtestlisadevC221720C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISAMCPWorkbenchtestlisadevC221720C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "s3:DeleteObject*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISAMCPWorkbenchtestlisadevC221720C", + "Arn" + ] + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "McpWorkbenchLambdaExecutionRoleDefaultPolicyAB1DECE8", + "Roles": [ + { + "Ref": "McpWorkbenchLambdaExecutionRole43E4060B" + } + ] + } + }, + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchlist046D8BF5": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Lists available MCP Workbench tools", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "WORKBENCH_BUCKET": { + "Ref": "LISAMCPWorkbenchtestlisadevC221720C" + } + } + }, + "FunctionName": "LisaMcpWorkbench-mcp_workbench-list", + "Handler": "mcp_workbench.lambda_functions.list", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpWorkbenchLambdaExecutionRole43E4060B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpWorkbenchLambdaExecutionRoleDefaultPolicyAB1DECE8", + "McpWorkbenchLambdaExecutionRole43E4060B" + ] + }, + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchcreateE9F26E3A": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Create MCP Workbench tools", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "WORKBENCH_BUCKET": { + "Ref": "LISAMCPWorkbenchtestlisadevC221720C" + } + } + }, + "FunctionName": "LisaMcpWorkbench-mcp_workbench-create", + "Handler": "mcp_workbench.lambda_functions.create", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpWorkbenchLambdaExecutionRole43E4060B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpWorkbenchLambdaExecutionRoleDefaultPolicyAB1DECE8", + "McpWorkbenchLambdaExecutionRole43E4060B" + ] + }, + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchread7EECA39E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Get MCP Workbench tool", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "WORKBENCH_BUCKET": { + "Ref": "LISAMCPWorkbenchtestlisadevC221720C" + } + } + }, + "FunctionName": "LisaMcpWorkbench-mcp_workbench-read", + "Handler": "mcp_workbench.lambda_functions.read", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpWorkbenchLambdaExecutionRole43E4060B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpWorkbenchLambdaExecutionRoleDefaultPolicyAB1DECE8", + "McpWorkbenchLambdaExecutionRole43E4060B" + ] + }, + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchupdate830E4B0B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Update MCP Workbench tool", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "WORKBENCH_BUCKET": { + "Ref": "LISAMCPWorkbenchtestlisadevC221720C" + } + } + }, + "FunctionName": "LisaMcpWorkbench-mcp_workbench-update", + "Handler": "mcp_workbench.lambda_functions.update", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpWorkbenchLambdaExecutionRole43E4060B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpWorkbenchLambdaExecutionRoleDefaultPolicyAB1DECE8", + "McpWorkbenchLambdaExecutionRole43E4060B" + ] + }, + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchdelete020452F9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Delete MCP Workbench tool", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "WORKBENCH_BUCKET": { + "Ref": "LISAMCPWorkbenchtestlisadevC221720C" + } + } + }, + "FunctionName": "LisaMcpWorkbench-mcp_workbench-delete", + "Handler": "mcp_workbench.lambda_functions.delete", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpWorkbenchLambdaExecutionRole43E4060B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpWorkbenchLambdaExecutionRoleDefaultPolicyAB1DECE8", + "McpWorkbenchLambdaExecutionRole43E4060B" + ] + }, + "McpWorkbenchLisaMcpWorkbenchmcpworkbenchvalidatesyntax4307220C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Validate Python code syntax", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "WORKBENCH_BUCKET": { + "Ref": "LISAMCPWorkbenchtestlisadevC221720C" + } + } + }, + "FunctionName": "LisaMcpWorkbench-mcp_workbench-validate_syntax", + "Handler": "mcp_workbench.lambda_functions.validate_syntax", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "McpWorkbenchLambdaExecutionRole43E4060B", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "McpWorkbenchLambdaExecutionRoleDefaultPolicyAB1DECE8", + "McpWorkbenchLambdaExecutionRole43E4060B" + ] + }, + "LISAMCPWorkbenchtestlisadevC221720C": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "test-lisa-dev-mcpworkbench-012345678901", + "LoggingConfiguration": { + "DestinationBucketName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + }, + "LogFilePrefix": "logs/mcpworkbench-bucket/" + }, + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LISAMCPWorkbenchtestlisadevPolicyF855EEF1": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "LISAMCPWorkbenchtestlisadevC221720C" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "LISAMCPWorkbenchtestlisadevC221720C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISAMCPWorkbenchtestlisadevC221720C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:PutBucketPolicy", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "LISAMCPWorkbenchtestlisadevC221720C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISAMCPWorkbenchtestlisadevC221720C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "LISAMCPWorkbenchtestlisadevAutoDeleteObjectsCustomResourceE8C041D3": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "LISAMCPWorkbenchtestlisadevC221720C" + } + }, + "DependsOn": [ + "LISAMCPWorkbenchtestlisadevPolicyF855EEF1" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LISAMCPWorkbenchtestlisadevNotificationsE32DE7AA": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "LISAMCPWorkbenchtestlisadevC221720C" + }, + "NotificationConfiguration": { + "EventBridgeConfiguration": {} + }, + "Managed": true, + "SkipDestinationValidation": false + }, + "DependsOn": [ + "LISAMCPWorkbenchtestlisadevPolicyF855EEF1" + ] + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": "nodejs22.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "LISAMCPWorkbenchtestlisadevC221720C" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": { + "Type": "AWS::IAM::Role", + "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" + ] + ] + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LISAMCPWorkbenchtestlisadevC221720C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": [ + { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", + "Code": { + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n skipDestinationValidation = props.get('SkipDestinationValidation', 'false').lower() == 'true'\n stack_id = event['StackId']\n old = event.get(\"OldResourceProperties\", {}).get(\"NotificationConfiguration\", {})\n if managed:\n config = handle_managed(event[\"RequestType\"], notification_configuration)\n else:\n config = handle_unmanaged(props[\"BucketName\"], stack_id, event[\"RequestType\"], notification_configuration, old)\n s3.put_bucket_notification_configuration(Bucket=props[\"BucketName\"], NotificationConfiguration=config, SkipDestinationValidation=skipDestinationValidation)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration, old):\n def get_id(n):\n n['Id'] = ''\n sorted_notifications = sort_filter_rules(n)\n strToHash=json.dumps(sorted_notifications, sort_keys=True).replace('\"Name\": \"prefix\"', '\"Name\": \"Prefix\"').replace('\"Name\": \"suffix\"', '\"Name\": \"Suffix\"')\n return f\"{stack_id}-{hash(strToHash)}\"\n def with_id(n):\n n['Id'] = get_id(n)\n return n\n\n external_notifications = {}\n existing_notifications = s3.get_bucket_notification_configuration(Bucket=bucket)\n for t in CONFIGURATION_TYPES:\n if request_type == 'Update':\n old_incoming_ids = [get_id(n) for n in old.get(t, [])]\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not get_id(n) in old_incoming_ids] \n elif request_type == 'Delete':\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n elif request_type == 'Create':\n external_notifications[t] = [n for n in existing_notifications.get(t, [])]\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n if request_type == 'Delete':\n return external_notifications\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n\ndef sort_filter_rules(json_obj):\n if not isinstance(json_obj, dict):\n return json_obj\n for key, value in json_obj.items():\n if isinstance(value, dict):\n json_obj[key] = sort_filter_rules(value)\n elif isinstance(value, list):\n json_obj[key] = [sort_filter_rules(item) for item in value]\n if \"Filter\" in json_obj and \"Key\" in json_obj[\"Filter\"] and \"FilterRules\" in json_obj[\"Filter\"][\"Key\"]:\n filter_rules = json_obj[\"Filter\"][\"Key\"][\"FilterRules\"]\n sorted_filter_rules = sorted(filter_rules, key=lambda x: x[\"Name\"])\n json_obj[\"Filter\"][\"Key\"][\"FilterRules\"] = sorted_filter_rules\n return json_obj" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn" + ] + }, + "Runtime": "python3.13", + "Timeout": 300 + }, + "DependsOn": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + ] + }, + "McpWorkbenchServiceS3EventHandlerRole49FEE688": { + "Type": "AWS::IAM::Role", + "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": "arn:aws:logs:*:*:*" + }, + { + "Action": [ + "ecs:UpdateService", + "ecs:DescribeServices", + "ecs:DescribeClusters" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:ecs:us-iso-east-1:*:cluster/", + { + "Fn::ImportValue": "LisaServe:ExportsOutputRefRestApiECSClustertestlisadevClC04148B6699D280E" + }, + "*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:ecs:us-iso-east-1:*:service/", + { + "Fn::ImportValue": "LisaServe:ExportsOutputRefRestApiECSClustertestlisadevClC04148B6699D280E" + }, + "*/", + { + "Fn::ImportValue": "LisaServe:ExportsOutputFnGetAttRestApiECSClustertestlisaMCPWORKBENCHEc2SvcService1642D1D0Name81C9F72A" + }, + "*" + ] + ] + } + ] + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": "arn:aws:ssm:us-iso-east-1:*:parameter/dev/test-lisa/lisa/deploymentName" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "S3EventHandlerPolicy" + } + ] + } + }, + "McpWorkbenchServiceS3EventHandlerLambda833B80DB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "DEPLOYMENT_PREFIX": "/dev/test-lisa/lisa", + "API_NAME": "MCPWorkbench", + "ECS_CLUSTER_NAME": { + "Fn::ImportValue": "LisaServe:ExportsOutputRefRestApiECSClustertestlisadevClC04148B6699D280E" + }, + "MCPWORKBENCH_SERVICE_NAME": { + "Fn::ImportValue": "LisaServe:ExportsOutputFnGetAttRestApiECSClustertestlisaMCPWORKBENCHEc2SvcService1642D1D0Name81C9F72A" + } + } + }, + "Handler": "mcp_workbench.s3_event_handler.handler", + "Role": { + "Fn::GetAtt": [ + "McpWorkbenchServiceS3EventHandlerRole49FEE688", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 120 + }, + "DependsOn": [ + "McpWorkbenchServiceS3EventHandlerRole49FEE688" + ] + }, + "McpWorkbenchServiceRescanMCPWorkbenchRule7B9F0946": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.s3", + "debug" + ], + "detail-type": [ + "Object Created", + "Object Deleted" + ], + "detail": { + "bucket": { + "name": [ + "test-lisa-dev-mcpworkbench-012345678901" + ] + } + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "McpWorkbenchServiceS3EventHandlerLambda833B80DB", + "Arn" + ] + }, + "Id": "Target0", + "RetryPolicy": { + "MaximumEventAgeInSeconds": 300, + "MaximumRetryAttempts": 2 + } + } + ] + } + }, + "McpWorkbenchServiceRescanMCPWorkbenchRuleAllowEventRuleLisaMcpWorkbenchMcpWorkbenchServiceS3EventHandlerLambdaDAC36E6D19509B7A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "McpWorkbenchServiceS3EventHandlerLambda833B80DB", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "McpWorkbenchServiceRescanMCPWorkbenchRule7B9F0946", + "Arn" + ] + } + } + } + }, + "Parameters": { + "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/bucket/bucket-access-logs" + }, + "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/layerVersion/common" + }, + "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/layerVersion/fastapi" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "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/test/cdk/stacks/__baselines__/LisaMetrics.json b/test/cdk/stacks/__baselines__/LisaMetrics.json new file mode 100644 index 000000000..f81526c99 --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaMetrics.json @@ -0,0 +1,935 @@ +{ + "Description": "LISA-metrics: test-lisa-dev", + "Resources": { + "LisaMetricsUsageMetricsTableFA0CC982": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "userId", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "userId", + "KeyType": "HASH" + } + ], + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LisaMetricsUsageMetricsTableNameParameter507381B6": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/table/usage-metrics", + "Type": "String", + "Value": { + "Ref": "LisaMetricsUsageMetricsTableFA0CC982" + } + } + }, + "LisaMetricsUsageMetricsQueue9010605A": { + "Type": "AWS::SQS::Queue", + "Properties": { + "MessageRetentionPeriod": 1209600, + "VisibilityTimeout": 120 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LisaMetricsUsageMetricsQueueName81C4433E": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/queue-name/usage-metrics", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "LisaMetricsUsageMetricsQueue9010605A", + "QueueName" + ] + } + } + }, + "LisaMetricsRestApimetricsCC0E74CE": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "metrics", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "LisaMetricsRestApimetricsOPTIONS37E920B8": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "LisaMetricsRestApimetricsCC0E74CE" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "LisaMetricsRestApimetricsusers8F0A6BFD": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "LisaMetricsRestApimetricsCC0E74CE" + }, + "PathPart": "users", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "LisaMetricsRestApimetricsusersOPTIONS43F6FBEA": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "LisaMetricsRestApimetricsusers8F0A6BFD" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "LisaMetricsRestApimetricsusersuserIdB778B043": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "LisaMetricsRestApimetricsusers8F0A6BFD" + }, + "PathPart": "{userId}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "LisaMetricsRestApimetricsusersuserIdOPTIONS58FBDBA2": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "LisaMetricsRestApimetricsusersuserIdB778B043" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "LisaMetricsRestApimetricsusersuserIdGETApiPermissionLisaMetricsRestApi17E18DF9GETmetricsusersuserIdECF59278": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LisaMetricsLisaMetricsmetricsgetusermetricsA7DCD5D4", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/metrics/users/*" + ] + ] + } + } + }, + "LisaMetricsRestApimetricsusersuserIdGETApiPermissionTestLisaMetricsRestApi17E18DF9GETmetricsusersuserIdDE71D356": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LisaMetricsLisaMetricsmetricsgetusermetricsA7DCD5D4", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/metrics/users/*" + ] + ] + } + } + }, + "LisaMetricsRestApimetricsusersuserIdGETCC33ABE5": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LisaMetricsLisaMetricsmetricsgetusermetricsA7DCD5D4", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "LisaMetricsRestApimetricsusersuserIdB778B043" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "LisaMetricsRestApimetricsusersallD9D38004": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "LisaMetricsRestApimetricsusers8F0A6BFD" + }, + "PathPart": "all", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "LisaMetricsRestApimetricsusersallOPTIONS20F4B13C": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "LisaMetricsRestApimetricsusersallD9D38004" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "LisaMetricsRestApimetricsusersallGETApiPermissionLisaMetricsRestApi17E18DF9GETmetricsusersall62FD36E1": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LisaMetricsLisaMetricsmetricsgetusermetricsall5D030B37", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/metrics/users/all" + ] + ] + } + } + }, + "LisaMetricsRestApimetricsusersallGETApiPermissionTestLisaMetricsRestApi17E18DF9GETmetricsusersallCD8ADEFF": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LisaMetricsLisaMetricsmetricsgetusermetricsall5D030B37", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/metrics/users/all" + ] + ] + } + } + }, + "LisaMetricsRestApimetricsusersallGETDDB51BEA": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LisaMetricsLisaMetricsmetricsgetusermetricsall5D030B37", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "LisaMetricsRestApimetricsusersallD9D38004" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "LisaMetricsUsageMetricsDashboardE9BF4FEA": { + "Type": "AWS::CloudWatch::Dashboard", + "Properties": { + "DashboardBody": { + "Fn::Join": [ + "", + [ + "{\"start\":\"-P7D\",\"widgets\":[{\"type\":\"text\",\"width\":24,\"height\":1,\"x\":0,\"y\":0,\"properties\":{\"markdown\":\"# LISA Metrics Dashboard\"}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":0,\"y\":1,\"properties\":{\"view\":\"timeSeries\",\"title\":\"Total Prompts\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[\"LISA/UsageMetrics\",\"TotalPromptCount\",{\"period\":3600,\"stat\":\"Sum\"}]],\"yAxis\":{}}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":12,\"y\":1,\"properties\":{\"view\":\"timeSeries\",\"title\":\"Total RAG Usage\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[\"LISA/UsageMetrics\",\"RAGUsageCount\",{\"period\":3600,\"stat\":\"Sum\"}]],\"yAxis\":{}}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":0,\"y\":7,\"properties\":{\"view\":\"timeSeries\",\"title\":\"Total MCP Tool Calls\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[\"LISA/UsageMetrics\",\"TotalMCPToolCalls\",{\"period\":3600,\"stat\":\"Sum\"}]],\"yAxis\":{}}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":12,\"y\":7,\"properties\":{\"view\":\"timeSeries\",\"title\":\"Prompts by User\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[{\"expression\":\"SEARCH('{LISA/UsageMetrics,UserId} MetricName=\\\"UserPromptCount\\\"', 'Sum', 3600)\",\"period\":3600}]],\"yAxis\":{}}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":0,\"y\":13,\"properties\":{\"view\":\"timeSeries\",\"title\":\"RAG Usage by User\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[{\"expression\":\"SEARCH('{LISA/UsageMetrics,UserId} MetricName=\\\"UserRAGUsageCount\\\"', 'Sum', 3600)\",\"period\":3600}]],\"yAxis\":{}}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":12,\"y\":13,\"properties\":{\"view\":\"timeSeries\",\"title\":\"MCP Tool Calls by User\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[{\"expression\":\"SEARCH('{LISA/UsageMetrics,UserId} MetricName=\\\"UserMCPToolCalls\\\"', 'Sum', 3600)\",\"period\":3600}]],\"yAxis\":{}}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":0,\"y\":19,\"properties\":{\"view\":\"timeSeries\",\"title\":\"MCP Tool Calls by Tool\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[{\"expression\":\"SEARCH('{LISA/UsageMetrics,ToolName} MetricName=\\\"MCPToolCallsByTool\\\"', 'Sum', 3600)\",\"period\":3600}]],\"yAxis\":{}}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":12,\"y\":19,\"properties\":{\"view\":\"singleValue\",\"title\":\"Total User Count\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[\"LISA/UsageMetrics\",\"UniqueUsers\",{\"period\":86400,\"stat\":\"Maximum\"}]]}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":0,\"y\":25,\"properties\":{\"view\":\"pie\",\"title\":\"Groups by Membership Count\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[{\"expression\":\"SEARCH('{LISA/UsageMetrics,GroupName} MetricName=\\\"UsersPerGroup\\\"', 'Maximum', 86400)\",\"period\":86400}]],\"yAxis\":{}}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":12,\"y\":25,\"properties\":{\"view\":\"timeSeries\",\"title\":\"Group Prompt Counts\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[{\"expression\":\"SEARCH('{LISA/UsageMetrics,GroupName} MetricName=\\\"GroupPromptCount\\\"', 'Sum', 3600)\",\"period\":3600}]],\"yAxis\":{}}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":0,\"y\":31,\"properties\":{\"view\":\"timeSeries\",\"title\":\"Group RAG Usage\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[{\"expression\":\"SEARCH('{LISA/UsageMetrics,GroupName} MetricName=\\\"GroupRAGUsageCount\\\"', 'Sum', 3600)\",\"period\":3600}]],\"yAxis\":{}}},{\"type\":\"metric\",\"width\":12,\"height\":6,\"x\":12,\"y\":31,\"properties\":{\"view\":\"timeSeries\",\"title\":\"Group MCP Usage\",\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[{\"expression\":\"SEARCH('{LISA/UsageMetrics,GroupName} MetricName=\\\"GroupMCPToolCalls\\\"', 'Sum', 3600)\",\"period\":3600}]],\"yAxis\":{}}}]}" + ] + ] + }, + "DashboardName": "test-lisa-dev-LISA-Metrics" + } + }, + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleB2BF8012": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Role used by LISA LisaMetricsApiLambdaExecutionRole lambdas to access AWS resources", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable", + "dynamodb:GetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "LisaMetricsUsageMetricsTableFA0CC982", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LisaMetricsUsageMetricsTableFA0CC982", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "lambdaPermissions" + } + ], + "RoleName": "test-lisa-LisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRo" + } + }, + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleDefaultPolicyACF3471B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "cloudwatch:PutMetricData", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LisaMetricsUsageMetricsQueue9010605A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleDefaultPolicyACF3471B", + "Roles": [ + { + "Ref": "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleB2BF8012" + } + ] + } + }, + "LisaMetricsLisaMetricsmetricsgetusermetricsA7DCD5D4": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Gets metrics for a specific user", + "Environment": { + "Variables": { + "USAGE_METRICS_TABLE_NAME": { + "Ref": "LisaMetricsUsageMetricsTableFA0CC982" + } + } + }, + "FunctionName": "LisaMetrics-metrics-get_user_metrics", + "Handler": "metrics.lambda_functions.get_user_metrics", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleB2BF8012", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleDefaultPolicyACF3471B", + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleB2BF8012" + ] + }, + "LisaMetricsLisaMetricsmetricsgetusermetricsall5D030B37": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Gets aggregated metrics across all users", + "Environment": { + "Variables": { + "USAGE_METRICS_TABLE_NAME": { + "Ref": "LisaMetricsUsageMetricsTableFA0CC982" + } + } + }, + "FunctionName": "LisaMetrics-metrics-get_user_metrics_all", + "Handler": "metrics.lambda_functions.get_user_metrics_all", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleB2BF8012", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleDefaultPolicyACF3471B", + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleB2BF8012" + ] + }, + "LisaMetricsDailyMetricsLambda45184347": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "USAGE_METRICS_TABLE_NAME": { + "Ref": "LisaMetricsUsageMetricsTableFA0CC982" + } + } + }, + "Handler": "metrics/lambda_functions.daily_metrics_handler", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "Role": { + "Fn::GetAtt": [ + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleB2BF8012", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 120, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleDefaultPolicyACF3471B", + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleB2BF8012" + ] + }, + "LisaMetricsDailyMetricsLambdaEventRuleEEEACD3A": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(1 day)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "LisaMetricsDailyMetricsLambda45184347", + "Arn" + ] + }, + "Id": "Target0" + } + ] + } + }, + "LisaMetricsDailyMetricsLambdaEventRuleAllowEventRuleLisaMetricsDailyMetricsLambdaF56BE64BEE788BCB": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LisaMetricsDailyMetricsLambda45184347", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "LisaMetricsDailyMetricsLambdaEventRuleEEEACD3A", + "Arn" + ] + } + } + }, + "LisaMetricsUsageMetricsProcessorCC2E38B3": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "USAGE_METRICS_TABLE_NAME": { + "Ref": "LisaMetricsUsageMetricsTableFA0CC982" + } + } + }, + "Handler": "metrics.lambda_functions.process_metrics_sqs_event", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "Role": { + "Fn::GetAtt": [ + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleB2BF8012", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 120, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleDefaultPolicyACF3471B", + "LisaMetricsLisaLisaMetricsApiLambdaExecutionRoleLambdaExecutionRoleB2BF8012" + ] + }, + "LisaMetricsUsageMetricsProcessorSqsEventSourceLisaMetricsUsageMetricsQueue218BA80867087405": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 10, + "EventSourceArn": { + "Fn::GetAtt": [ + "LisaMetricsUsageMetricsQueue9010605A", + "Arn" + ] + }, + "FunctionName": { + "Ref": "LisaMetricsUsageMetricsProcessorCC2E38B3" + } + } + } + }, + "Parameters": { + "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/layerVersion/common" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "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/test/cdk/stacks/__baselines__/LisaModels.json b/test/cdk/stacks/__baselines__/LisaModels.json new file mode 100644 index 000000000..ba5a7f511 --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaModels.json @@ -0,0 +1,4097 @@ +{ + "Resources": { + "ModelsApiRestApimodelsC1697580": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "models", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApimodelsOPTIONS5A796DE8": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "ModelsApiRestApimodelsC1697580" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApimodelsproxy5167503B": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "ModelsApiRestApimodelsC1697580" + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApimodelsproxyOPTIONS87D3B81B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "ModelsApiRestApimodelsproxy5167503B" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApimodelsproxyANYApiPermissionLisaModelsModelsApiRestApi2F36137AANYmodelsproxy93282F24": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelshandler36D33339", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/*/models/*" + ] + ] + } + } + }, + "ModelsApiRestApimodelsproxyANYApiPermissionTestLisaModelsModelsApiRestApi2F36137AANYmodelsproxyD1C02325": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelshandler36D33339", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/*/models/*" + ] + ] + } + } + }, + "ModelsApiRestApimodelsproxyANY6E9F83E5": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "ANY", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelshandler36D33339", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "ModelsApiRestApimodelsproxy5167503B" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApimodelsGETB3D039CA": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelshandler36D33339", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "ModelsApiRestApimodelsC1697580" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApimodelsPOST3CF25A3C": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "POST", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelshandler36D33339", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "ModelsApiRestApimodelsC1697580" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApidocsC105FAE1": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "docs", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApidocsOPTIONS1FA68409": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "ModelsApiRestApidocsC105FAE1" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApidocsGETApiPermissionLisaModelsModelsApiRestApi2F36137AGETdocs7FD44A2B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelsdocs57EF06DB", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/docs" + ] + ] + } + } + }, + "ModelsApiRestApidocsGETApiPermissionTestLisaModelsModelsApiRestApi2F36137AGETdocsAE2A1BD3": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelsdocs57EF06DB", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/docs" + ] + ] + } + } + }, + "ModelsApiRestApidocsGETFEF5AB72": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "NONE", + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelsdocs57EF06DB", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "ModelsApiRestApidocsC105FAE1" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApiopenapijsonD9AE9024": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "openapi.json", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApiopenapijsonOPTIONS1255CA22": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "ModelsApiRestApiopenapijsonD9AE9024" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiRestApiopenapijsonGETFEBC2C73": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "NONE", + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelshandler36D33339", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "ModelsApiRestApiopenapijsonD9AE9024" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "ModelsApiModelTable72B9582E": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "model_id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "model_id", + "KeyType": "HASH" + } + ], + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ModelsApiModelTableNameParameterF3E005A6": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/modelTableName", + "Type": "String", + "Value": { + "Ref": "ModelsApiModelTable72B9582E" + } + } + }, + "ModelsApiecsmodelbuildrepo74B1906D": { + "Type": "AWS::ECR::Repository", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerroleD506279E": { + "Type": "AWS::IAM::Role", + "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/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": "arn:*:iam::*:role/cdk-*" + }, + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeSubnets", + "ec2:DeleteNetworkInterface", + "ec2:AssignPrivateIpAddresses", + "ec2:UnassignPrivateIpAddresses" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "lambdaPermissions" + } + ] + } + }, + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerFn9C16A03D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "def2b495b7c3e55fb5cb52b4b1a5f1b6494df311eb33a447447cd779f136122b.zip" + }, + "Environment": { + "Variables": { + "LISA_VPC_ID": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + }, + "LISA_SECURITY_GROUP_ID": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + }, + "LISA_CONFIG": "{\"appName\":\"lisa\",\"deploymentName\":\"test-lisa\",\"deploymentPrefix\":\"/dev/test-lisa/lisa\",\"region\":\"us-iso-east-1\",\"deploymentStage\":\"dev\",\"removalPolicy\":\"destroy\",\"s3BucketModels\":\"hf-models-gaiic\",\"mountS3DebUrl\":\"https://s3.amazonaws.com/mountpoint-s3-release/latest/x86_64/mount-s3.deb\",\"certificateAuthorityBundle\":\"\",\"pypiConfig\":{\"indexUrl\":\"localhost:8080\",\"trustedHost\":\"localhost\"},\"nvmeContainerMountPath\":\"/nvme\",\"nvmeHostMountPath\":\"/nvme\",\"condaUrl\":\"\"}" + } + }, + "EphemeralStorage": { + "Size": 2048 + }, + "FunctionName": "LisaModels-ecs_model_deployer-Fn", + "Handler": "index.handler", + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerroleD506279E", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 600, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerroleD506279E" + ] + }, + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketA3074A95": { + "Type": "AWS::S3::Bucket", + "Properties": { + "LoggingConfiguration": { + "DestinationBucketName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + } + }, + "Tags": [ + { + "Key": "aws-cdk:cr-owned:97c50996", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketPolicy0C3E0058": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketA3074A95" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketA3074A95", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketA3074A95", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2dplmntAwsCliLayerB03BDD23": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "c49d356cac773d491c5f7ac148995a1181498a8e289429f8612a7f7e3814f535.zip" + }, + "Description": "/opt/awscli/aws" + } + }, + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2dplmntCustomResourceCBF35CCE": { + "Type": "Custom::CDKBucketDeployment", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", + "Arn" + ] + }, + "SourceBucketNames": [ + "cdk-hnb659fds-assets-012345678901-us-iso-east-1" + ], + "SourceObjectKeys": [ + "9effa906703c74def5a9bbf3a3db197be8dfacbea4d2d0d822b510eb7ef50b80.zip" + ], + "DestinationBucketName": { + "Ref": "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketA3074A95" + }, + "WaitForDistributionInvalidation": true, + "Prune": true, + "OutputObjectKeys": true + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2role48B644BC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "RoleName": "LisaModels-docker-image-builder-ec2-role" + } + }, + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2policy17F11E25": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketA3074A95", + "Arn" + ] + }, + "/*" + ] + ] + } + }, + { + "Action": "s3:ListBucket", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketA3074A95", + "Arn" + ] + } + }, + { + "Action": [ + "ecr:GetAuthorizationToken", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "ecr:PutImage", + "ecr:BatchCheckLayerAvailability" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2policy17F11E25", + "Roles": [ + { + "Ref": "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2role48B644BC" + } + ] + } + }, + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderrole27133073": { + "Type": "AWS::IAM::Role", + "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" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "RoleName": "LisaModels-docker_image_builder_role" + } + }, + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderpolicyA4349424": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:RunInstances", + "ec2:CreateTags", + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeSubnets", + "ec2:DeleteNetworkInterface", + "ec2:AssignPrivateIpAddresses", + "ec2:UnassignPrivateIpAddresses" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2role48B644BC", + "Arn" + ] + } + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": "arn:*:ssm:*::parameter/aws/service/*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderpolicyA4349424", + "Roles": [ + { + "Ref": "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderrole27133073" + } + ] + } + }, + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderprofile7E79BDC1": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "InstanceProfileName": "LisaModels-docker-image-builder-profile", + "Roles": [ + { + "Ref": "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2role48B644BC" + } + ] + } + }, + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilder35917FEB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "LISA_DOCKER_BUCKET": { + "Ref": "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketA3074A95" + }, + "LISA_ECR_URI": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "ModelsApiecsmodelbuildrepo74B1906D", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "ModelsApiecsmodelbuildrepo74B1906D", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "ModelsApiecsmodelbuildrepo74B1906D" + } + ] + ] + }, + "LISA_INSTANCE_PROFILE": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderprofile7E79BDC1", + "Arn" + ] + }, + "LISA_MOUNTS3_DEB_URL": "https://s3.amazonaws.com/mountpoint-s3-release/latest/x86_64/mount-s3.deb", + "LISA_IMAGEBUILDER_VOLUME_SIZE": "50" + } + }, + "FunctionName": "LisaModels-docker-image-builder", + "Handler": "dockerimagebuilder.handler", + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderrole27133073", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderrole27133073" + ] + }, + "ModelsApiModelsSfnLambdaRoleF400F0BC": { + "Type": "AWS::IAM::Role", + "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/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:DeleteItem", + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:Scan" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiModelTable72B9582E", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiModelTable72B9582E", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "cloudformation:CreateStack", + "cloudformation:DeleteStack", + "cloudformation:DescribeStacks" + ], + "Effect": "Allow", + "Resource": "arn:*:cloudformation:*:*:stack/*" + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilder35917FEB", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerFn9C16A03D", + "Arn" + ] + } + ] + }, + { + "Action": [ + "ecr:DescribeImages", + "ecr:DescribeRepositories", + "ecr:GetRepositoryPolicy", + "ecr:ListImages" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeSubnets", + "ec2:DeleteNetworkInterface", + "ec2:AssignPrivateIpAddresses", + "ec2:UnassignPrivateIpAddresses" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "ec2:TerminateInstances", + "Condition": { + "StringEquals": { + "aws:ResourceTag/lisa_temporary_instance": "true" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "secretsmanager:GetSecretValue", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":secretsmanager:us-iso-east-1:012345678901:secret:", + { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "-??????" + ] + ] + } + }, + { + "Action": [ + "autoscaling:DescribeAutoScalingGroups", + "autoscaling:UpdateAutoScalingGroup" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:DescribeTaskDefinition", + "ecs:RegisterTaskDefinition", + "ecs:UpdateService", + "ecs:DescribeServices" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "cloudformation:DescribeStackResources", + "Effect": "Allow", + "Resource": "arn:*:cloudformation:*:*:stack/*" + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:us-iso-east-1:012345678901:parameter/dev/test-lisa/lisa/lisaServeRestApiUri" + ] + ] + }, + "arn:aws:ssm:us-iso-east-1:012345678901:parameter/dev/test-lisa/lisa/lisaServeRestApiUri", + "arn:aws:ssm:us-iso-east-1:012345678901:parameter/LISA-lisa-management-key", + "arn:aws:ssm:us-iso-east-1:012345678901:parameter/dev/test-lisa/lisa/LiteLLMDbConnectionInfo", + "arn:aws:ssm:us-iso-east-1:012345678901:parameter/dev/test-lisa/lisa/modelTableName" + ] + }, + { + "Action": "secretsmanager:GetSecretValue", + "Effect": "Allow", + "Resource": "arn:aws:secretsmanager:us-iso-east-1:012345678901:secret:*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "lambdaPermissions" + } + ] + } + }, + "ModelsApiCreateModelWorkflowSetModelToCreatingFunc4E8D1CA0": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "DOCKER_IMAGE_BUILDER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilder35917FEB", + "Arn" + ] + }, + "ECR_REPOSITORY_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodelbuildrepo74B1906D", + "Arn" + ] + }, + "ECR_REPOSITORY_NAME": { + "Ref": "ModelsApiecsmodelbuildrepo74B1906D" + }, + "ECS_MODEL_DEPLOYER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerFn9C16A03D", + "Arn" + ] + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}", + "AWS_ACCOUNT_ID": "012345678901", + "AWS_PARTITION": "aws" + } + }, + "Handler": "models.state_machine.create_model.handle_set_model_to_creating", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiCreateModelWorkflowStartCopyDockerImageFuncE508BA76": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "DOCKER_IMAGE_BUILDER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilder35917FEB", + "Arn" + ] + }, + "ECR_REPOSITORY_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodelbuildrepo74B1906D", + "Arn" + ] + }, + "ECR_REPOSITORY_NAME": { + "Ref": "ModelsApiecsmodelbuildrepo74B1906D" + }, + "ECS_MODEL_DEPLOYER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerFn9C16A03D", + "Arn" + ] + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}", + "AWS_ACCOUNT_ID": "012345678901", + "AWS_PARTITION": "aws" + } + }, + "Handler": "models.state_machine.create_model.handle_start_copy_docker_image", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiCreateModelWorkflowPollDockerImageAvailableFuncF23F9A33": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "DOCKER_IMAGE_BUILDER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilder35917FEB", + "Arn" + ] + }, + "ECR_REPOSITORY_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodelbuildrepo74B1906D", + "Arn" + ] + }, + "ECR_REPOSITORY_NAME": { + "Ref": "ModelsApiecsmodelbuildrepo74B1906D" + }, + "ECS_MODEL_DEPLOYER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerFn9C16A03D", + "Arn" + ] + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}", + "AWS_ACCOUNT_ID": "012345678901", + "AWS_PARTITION": "aws" + } + }, + "Handler": "models.state_machine.create_model.handle_poll_docker_image_available", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiCreateModelWorkflowHandleFailureFunc7CC3D0A8": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "DOCKER_IMAGE_BUILDER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilder35917FEB", + "Arn" + ] + }, + "ECR_REPOSITORY_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodelbuildrepo74B1906D", + "Arn" + ] + }, + "ECR_REPOSITORY_NAME": { + "Ref": "ModelsApiecsmodelbuildrepo74B1906D" + }, + "ECS_MODEL_DEPLOYER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerFn9C16A03D", + "Arn" + ] + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}", + "AWS_ACCOUNT_ID": "012345678901", + "AWS_PARTITION": "aws" + } + }, + "Handler": "models.state_machine.create_model.handle_failure", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiCreateModelWorkflowStartCreateStackFuncCEE91381": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "DOCKER_IMAGE_BUILDER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilder35917FEB", + "Arn" + ] + }, + "ECR_REPOSITORY_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodelbuildrepo74B1906D", + "Arn" + ] + }, + "ECR_REPOSITORY_NAME": { + "Ref": "ModelsApiecsmodelbuildrepo74B1906D" + }, + "ECS_MODEL_DEPLOYER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerFn9C16A03D", + "Arn" + ] + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}", + "AWS_ACCOUNT_ID": "012345678901", + "AWS_PARTITION": "aws" + } + }, + "Handler": "models.state_machine.create_model.handle_start_create_stack", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 480, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiCreateModelWorkflowPollCreateStackFunc3B3660A0": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "DOCKER_IMAGE_BUILDER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilder35917FEB", + "Arn" + ] + }, + "ECR_REPOSITORY_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodelbuildrepo74B1906D", + "Arn" + ] + }, + "ECR_REPOSITORY_NAME": { + "Ref": "ModelsApiecsmodelbuildrepo74B1906D" + }, + "ECS_MODEL_DEPLOYER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerFn9C16A03D", + "Arn" + ] + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}", + "AWS_ACCOUNT_ID": "012345678901", + "AWS_PARTITION": "aws" + } + }, + "Handler": "models.state_machine.create_model.handle_poll_create_stack", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiCreateModelWorkflowAddModelToLitellmFunc6B8DBAE6": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "DOCKER_IMAGE_BUILDER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilder35917FEB", + "Arn" + ] + }, + "ECR_REPOSITORY_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodelbuildrepo74B1906D", + "Arn" + ] + }, + "ECR_REPOSITORY_NAME": { + "Ref": "ModelsApiecsmodelbuildrepo74B1906D" + }, + "ECS_MODEL_DEPLOYER_FN_ARN": { + "Fn::GetAtt": [ + "ModelsApiecsmodeldeployerLisaModelsecsmodeldeployerFn9C16A03D", + "Arn" + ] + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}", + "AWS_ACCOUNT_ID": "012345678901", + "AWS_PARTITION": "aws" + } + }, + "Handler": "models.state_machine.create_model.handle_add_model_to_litellm", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiCreateModelWorkflowCreateModelSMRoleA50933E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ModelsApiCreateModelWorkflowCreateModelSMRoleDefaultPolicy3AEF4263": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowSetModelToCreatingFunc4E8D1CA0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowSetModelToCreatingFunc4E8D1CA0", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowAddModelToLitellmFunc6B8DBAE6", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowAddModelToLitellmFunc6B8DBAE6", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowStartCopyDockerImageFuncE508BA76", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowStartCopyDockerImageFuncE508BA76", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowPollDockerImageAvailableFuncF23F9A33", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowPollDockerImageAvailableFuncF23F9A33", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowStartCreateStackFuncCEE91381", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowStartCreateStackFuncCEE91381", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowPollCreateStackFunc3B3660A0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowPollCreateStackFunc3B3660A0", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowHandleFailureFunc7CC3D0A8", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowHandleFailureFunc7CC3D0A8", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelsApiCreateModelWorkflowCreateModelSMRoleDefaultPolicy3AEF4263", + "Roles": [ + { + "Ref": "ModelsApiCreateModelWorkflowCreateModelSMRoleA50933E5" + } + ] + } + }, + "ModelsApiCreateModelWorkflowCreateModelSM80C5B2A6": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"SetModelToCreating\",\"States\":{\"SetModelToCreating\":{\"Next\":\"CreateModelInfraChoice\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowSetModelToCreatingFunc4E8D1CA0", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"CreateModelInfraChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.create_infra\",\"BooleanEquals\":true,\"Next\":\"StartCopyDockerImage\"}],\"Default\":\"AddModelToLitellm\"},\"AddModelToLitellm\":{\"Next\":\"CreateSuccess\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowAddModelToLitellmFunc6B8DBAE6", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"CreateSuccess\":{\"Type\":\"Succeed\"},\"StartCopyDockerImage\":{\"Next\":\"CheckImageTypeChoice\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Catch\":[{\"ErrorEquals\":[\"States.TaskFailed\"],\"Next\":\"HandleFailure\"}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowStartCopyDockerImageFuncE508BA76", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"CheckImageTypeChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.image_info.image_status\",\"StringEquals\":\"prebuilt\",\"Next\":\"StartCreateStack\"}],\"Default\":\"PollDockerImageAvailable\"},\"PollDockerImageAvailable\":{\"Next\":\"PollDockerImageChoice\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Catch\":[{\"ErrorEquals\":[\"MaxPollsExceededException\"],\"Next\":\"HandleFailure\"}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowPollDockerImageAvailableFuncF23F9A33", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"WaitBeforePollingDockerImage\":{\"Type\":\"Wait\",\"Seconds\":60,\"Next\":\"PollDockerImageAvailable\"},\"PollDockerImageChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.continue_polling_docker\",\"BooleanEquals\":true,\"Next\":\"WaitBeforePollingDockerImage\"}],\"Default\":\"StartCreateStack\"},\"StartCreateStack\":{\"Next\":\"PollCreateStack\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Catch\":[{\"ErrorEquals\":[\"StackFailedToCreateException\"],\"Next\":\"HandleFailure\"}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowStartCreateStackFuncCEE91381", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"PollCreateStack\":{\"Next\":\"PollCreateStackChoice\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Catch\":[{\"ErrorEquals\":[\"MaxPollsExceededException\",\"UnexpectedCloudFormationStateException\"],\"Next\":\"HandleFailure\"}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowPollCreateStackFunc3B3660A0", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"WaitBeforePollingCreateStack\":{\"Type\":\"Wait\",\"Seconds\":60,\"Next\":\"PollCreateStack\"},\"PollCreateStackChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.continue_polling_stack\",\"BooleanEquals\":true,\"Next\":\"WaitBeforePollingCreateStack\"}],\"Default\":\"AddModelToLitellm\"},\"HandleFailure\":{\"Next\":\"CreateFailed\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowHandleFailureFunc7CC3D0A8", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"CreateFailed\":{\"Type\":\"Fail\"}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "ModelsApiCreateModelWorkflowCreateModelSMRoleA50933E5", + "Arn" + ] + } + }, + "DependsOn": [ + "ModelsApiCreateModelWorkflowCreateModelSMRoleDefaultPolicy3AEF4263", + "ModelsApiCreateModelWorkflowCreateModelSMRoleA50933E5" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ModelsApiDeleteModelWorkflowSetModelToDeletingFuncCA1C7F8D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev" + } + }, + "Handler": "models.state_machine.delete_model.handle_set_model_to_deleting", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiDeleteModelWorkflowDeleteFromLitellmFunc75B8FA09": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev" + } + }, + "Handler": "models.state_machine.delete_model.handle_delete_from_litellm", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiDeleteModelWorkflowDeleteStackFunc0B8E2D75": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev" + } + }, + "Handler": "models.state_machine.delete_model.handle_delete_stack", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiDeleteModelWorkflowMonitorDeleteStackFunc2CE43E62": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev" + } + }, + "Handler": "models.state_machine.delete_model.handle_monitor_delete_stack", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiDeleteModelWorkflowDeleteFromDdbFuncAB2B6BFB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev" + } + }, + "Handler": "models.state_machine.delete_model.handle_delete_from_ddb", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiDeleteModelWorkflowDeleteModelSMRole51851444": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ModelsApiDeleteModelWorkflowDeleteModelSMRoleDefaultPolicy8A3C524C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowSetModelToDeletingFuncCA1C7F8D", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowSetModelToDeletingFuncCA1C7F8D", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowDeleteFromLitellmFunc75B8FA09", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowDeleteFromLitellmFunc75B8FA09", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowDeleteFromDdbFuncAB2B6BFB", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowDeleteFromDdbFuncAB2B6BFB", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowDeleteStackFunc0B8E2D75", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowDeleteStackFunc0B8E2D75", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowMonitorDeleteStackFunc2CE43E62", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowMonitorDeleteStackFunc2CE43E62", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelsApiDeleteModelWorkflowDeleteModelSMRoleDefaultPolicy8A3C524C", + "Roles": [ + { + "Ref": "ModelsApiDeleteModelWorkflowDeleteModelSMRole51851444" + } + ] + } + }, + "ModelsApiDeleteModelWorkflowDeleteModelSMDAC2A84A": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"SetModelToDeleting\",\"States\":{\"SetModelToDeleting\":{\"Next\":\"DeleteFromLitellm\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowSetModelToDeletingFuncCA1C7F8D", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"DeleteFromLitellm\":{\"Next\":\"DeleteStackChoice\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowDeleteFromLitellmFunc75B8FA09", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"DeleteStackChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.cloudformation_stack_arn\",\"IsNull\":false,\"Next\":\"DeleteStack\"}],\"Default\":\"DeleteFromDdb\"},\"DeleteFromDdb\":{\"Next\":\"DeleteSuccess\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowDeleteFromDdbFuncAB2B6BFB", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"DeleteSuccess\":{\"Type\":\"Succeed\"},\"DeleteStack\":{\"Next\":\"MonitorDeleteStack\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowDeleteStackFunc0B8E2D75", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"MonitorDeleteStack\":{\"Next\":\"PollDeleteStackChoice\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowMonitorDeleteStackFunc2CE43E62", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"WaitBeforePollDeleteStack\":{\"Type\":\"Wait\",\"Seconds\":60,\"Next\":\"MonitorDeleteStack\"},\"PollDeleteStackChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.continue_polling\",\"BooleanEquals\":true,\"Next\":\"WaitBeforePollDeleteStack\"}],\"Default\":\"DeleteFromDdb\"}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "ModelsApiDeleteModelWorkflowDeleteModelSMRole51851444", + "Arn" + ] + } + }, + "DependsOn": [ + "ModelsApiDeleteModelWorkflowDeleteModelSMRoleDefaultPolicy8A3C524C", + "ModelsApiDeleteModelWorkflowDeleteModelSMRole51851444" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ModelsApiUpdateModelWorkflowHandleJobIntakeFuncA1438F67": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}" + } + }, + "Handler": "models.state_machine.update_model.handle_job_intake", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiUpdateModelWorkflowHandlePollCapacityFunc5376513F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}" + } + }, + "Handler": "models.state_machine.update_model.handle_poll_capacity", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiUpdateModelWorkflowHandleEcsUpdateFunc1CF09788": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}" + } + }, + "Handler": "models.state_machine.update_model.handle_ecs_update", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiUpdateModelWorkflowHandlePollEcsDeploymentFuncDF9FFF3B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}" + } + }, + "Handler": "models.state_machine.update_model.handle_poll_ecs_deployment", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiUpdateModelWorkflowHandleFinishUpdateFunc92E550FB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + }, + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LITELLM_CONFIG_OBJ": "{\"db_key\":\"sk-012345\"}" + } + }, + "Handler": "models.state_machine.update_model.handle_finish_update", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiUpdateModelWorkflowUpdateModelSMRoleF6FA4558": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ModelsApiUpdateModelWorkflowUpdateModelSMRoleDefaultPolicy3F382CD9": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandleJobIntakeFuncA1438F67", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandleJobIntakeFuncA1438F67", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandleFinishUpdateFunc92E550FB", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandleFinishUpdateFunc92E550FB", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandlePollCapacityFunc5376513F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandlePollCapacityFunc5376513F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandleEcsUpdateFunc1CF09788", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandleEcsUpdateFunc1CF09788", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandlePollEcsDeploymentFuncDF9FFF3B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandlePollEcsDeploymentFuncDF9FFF3B", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelsApiUpdateModelWorkflowUpdateModelSMRoleDefaultPolicy3F382CD9", + "Roles": [ + { + "Ref": "ModelsApiUpdateModelWorkflowUpdateModelSMRoleF6FA4558" + } + ] + } + }, + "ModelsApiUpdateModelWorkflowUpdateModelSMDB7A1316": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"HandleJobIntake\",\"States\":{\"HandleJobIntake\":{\"Next\":\"HasEcsUpdateChoice\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandleJobIntakeFuncA1438F67", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"HasEcsUpdateChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.needs_ecs_update\",\"BooleanEquals\":true,\"Next\":\"HandleEcsUpdate\"}],\"Default\":\"HasCapacityUpdateChoice\"},\"HasCapacityUpdateChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.has_capacity_update\",\"BooleanEquals\":true,\"Next\":\"HandlePollCapacity\"}],\"Default\":\"HandleFinishUpdate\"},\"HandleFinishUpdate\":{\"Next\":\"UpdateSuccess\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandleFinishUpdateFunc92E550FB", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"WaitBeforeModelAvailable\":{\"Type\":\"Wait\",\"SecondsPath\":\"$.model_warmup_seconds\",\"Next\":\"HandleFinishUpdate\"},\"UpdateSuccess\":{\"Type\":\"Succeed\"},\"HandlePollCapacity\":{\"Next\":\"PollAsgChoice\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandlePollCapacityFunc5376513F", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"WaitBeforePollAsg\":{\"Type\":\"Wait\",\"Seconds\":60,\"Next\":\"HandlePollCapacity\"},\"PollAsgChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.should_continue_capacity_polling\",\"BooleanEquals\":true,\"Next\":\"WaitBeforePollAsg\"}],\"Default\":\"WaitBeforeModelAvailable\"},\"HandleEcsUpdate\":{\"Next\":\"HandlePollEcsDeployment\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandleEcsUpdateFunc1CF09788", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"HandlePollEcsDeployment\":{\"Next\":\"PollEcsDeploymentChoice\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowHandlePollEcsDeploymentFuncDF9FFF3B", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"WaitBeforePollEcsDeployment\":{\"Type\":\"Wait\",\"Seconds\":60,\"Next\":\"HandlePollEcsDeployment\"},\"PollEcsDeploymentChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.should_continue_ecs_polling\",\"BooleanEquals\":true,\"Next\":\"WaitBeforePollEcsDeployment\"}],\"Default\":\"HasCapacityUpdateChoice\"}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "ModelsApiUpdateModelWorkflowUpdateModelSMRoleF6FA4558", + "Arn" + ] + } + }, + "DependsOn": [ + "ModelsApiUpdateModelWorkflowUpdateModelSMRoleDefaultPolicy3F382CD9", + "ModelsApiUpdateModelWorkflowUpdateModelSMRoleF6FA4558" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ModelsApiLisaModelApiLambdaExecutionRole3E663E08": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Role used by LISA ModelApi lambdas to access AWS resources", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable", + "dynamodb:GetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiModelTable72B9582E", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiModelTable72B9582E", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "lambdaPermissions" + } + ], + "RoleName": "test-lisa-LisaModelApiLambdaExecutionRole" + } + }, + "ModelsApiLisaModelApiLambdaExecutionRoleDefaultPolicy1C96D538": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:us-iso-east-1:012345678901:parameter/dev/test-lisa/lisa/lisaServeRestApiUri" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelsApiLisaModelApiLambdaExecutionRoleDefaultPolicy1C96D538", + "Roles": [ + { + "Ref": "ModelsApiLisaModelApiLambdaExecutionRole3E663E08" + } + ] + } + }, + "ModelsApiLisaModelsmodelshandler36D33339": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Manage model", + "Environment": { + "Variables": { + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "CREATE_SFN_ARN": { + "Ref": "ModelsApiCreateModelWorkflowCreateModelSM80C5B2A6" + }, + "DELETE_SFN_ARN": { + "Ref": "ModelsApiDeleteModelWorkflowDeleteModelSMDAC2A84A" + }, + "UPDATE_SFN_ARN": { + "Ref": "ModelsApiUpdateModelWorkflowUpdateModelSMDB7A1316" + }, + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + } + } + }, + "FunctionName": "LisaModels-models-handler", + "Handler": "models.lambda_functions.handler", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "ModelsApiLisaModelApiLambdaExecutionRole3E663E08", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiLisaModelApiLambdaExecutionRoleDefaultPolicy1C96D538", + "ModelsApiLisaModelApiLambdaExecutionRole3E663E08" + ] + }, + "ModelsApiModelsApiCertPerms4642C7FC": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "iam:GetServerCertificate", + "Effect": "Allow", + "Resource": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelsApiModelsApiCertPerms4642C7FC", + "Roles": [ + { + "Ref": "ModelsApiLisaModelApiLambdaExecutionRole3E663E08" + }, + { + "Ref": "ModelsApiModelsSfnLambdaRoleF400F0BC" + } + ] + } + }, + "ModelsApiLambdaInvokeAccessRemoteLisaModelsmodelshandler4a9eC2C78690": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelshandler36D33339", + "Arn" + ] + } + ] + } + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/models" + ] + ] + } + } + }, + "ModelsApiLambdaInvokeAccessRemoteLisaModelsmodelshandler8e30CA5B0DC1": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelshandler36D33339", + "Arn" + ] + } + ] + } + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/POST/models" + ] + ] + } + } + }, + "ModelsApiLisaModelsmodelsdocs57EF06DB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Manage model", + "Environment": { + "Variables": { + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "REST_API_VERSION": "v2", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "CREATE_SFN_ARN": { + "Ref": "ModelsApiCreateModelWorkflowCreateModelSM80C5B2A6" + }, + "DELETE_SFN_ARN": { + "Ref": "ModelsApiDeleteModelWorkflowDeleteModelSMDAC2A84A" + }, + "UPDATE_SFN_ARN": { + "Ref": "ModelsApiUpdateModelWorkflowUpdateModelSMDB7A1316" + }, + "MODEL_TABLE_NAME": { + "Ref": "ModelsApiModelTable72B9582E" + } + } + }, + "FunctionName": "LisaModels-models-docs", + "Handler": "models.lambda_functions.docs", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "ModelsApiLisaModelApiLambdaExecutionRole3E663E08", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiLisaModelApiLambdaExecutionRoleDefaultPolicy1C96D538", + "ModelsApiLisaModelApiLambdaExecutionRole3E663E08" + ] + }, + "ModelsApiLambdaInvokeAccessRemoteLisaModelsmodelshandler5f34ADCFCD5F": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "ModelsApiLisaModelsmodelshandler36D33339", + "Arn" + ] + } + ] + } + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/openapi.json" + ] + ] + } + } + }, + "ModelsApiModelsApiStateMachinePerms95A4A4A1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": [ + { + "Ref": "ModelsApiCreateModelWorkflowCreateModelSM80C5B2A6" + }, + { + "Ref": "ModelsApiDeleteModelWorkflowDeleteModelSMDAC2A84A" + }, + { + "Ref": "ModelsApiUpdateModelWorkflowUpdateModelSMDB7A1316" + } + ] + }, + { + "Action": [ + "dynamodb:GetItem", + "dynamodb:Scan" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiModelTable72B9582E", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiModelTable72B9582E", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "autoscaling:DescribeAutoScalingGroups", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelsApiModelsApiStateMachinePerms95A4A4A1", + "Roles": [ + { + "Ref": "ModelsApiLisaModelApiLambdaExecutionRole3E663E08" + } + ] + } + }, + "ModelsApiModelApiKeyCleanup843C6643": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Remove api_key from existing Bedrock models to fix Invalid API Key format errors", + "Environment": { + "Variables": { + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "MANAGEMENT_KEY_NAME": { + "Ref": "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "REST_API_VERSION": "v2", + "DEPLOYMENT_PREFIX": "/dev/test-lisa/lisa" + } + }, + "Handler": "models.model_api_key_cleanup.lambda_handler", + "Layers": [ + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 300, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "ModelsApiModelsSfnLambdaRoleF400F0BC" + ] + }, + "ModelsApiModelApiKeyCleanupProviderframeworkonEventServiceRoleF5508E89": { + "Type": "AWS::IAM::Role", + "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" + ] + ] + } + ] + } + }, + "ModelsApiModelApiKeyCleanupProviderframeworkonEventServiceRoleDefaultPolicy13D5D897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApiModelApiKeyCleanup843C6643", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApiModelApiKeyCleanup843C6643", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:GetFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ModelsApiModelApiKeyCleanup843C6643", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelsApiModelApiKeyCleanupProviderframeworkonEventServiceRoleDefaultPolicy13D5D897", + "Roles": [ + { + "Ref": "ModelsApiModelApiKeyCleanupProviderframeworkonEventServiceRoleF5508E89" + } + ] + } + }, + "ModelsApiModelApiKeyCleanupProviderframeworkonEvent93A3C88F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "bdc104ed9cab1b5b6421713c8155f0b753380595356f710400609664d3635eca.zip" + }, + "Description": "AWS CDK resource provider framework - onEvent (LisaModels/ModelsApi/ModelApiKeyCleanupProvider)", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "ModelsApiModelApiKeyCleanup843C6643", + "Arn" + ] + } + } + }, + "Handler": "framework.onEvent", + "LoggingConfig": { + "ApplicationLogLevel": "FATAL", + "LogFormat": "JSON" + }, + "Role": { + "Fn::GetAtt": [ + "ModelsApiModelApiKeyCleanupProviderframeworkonEventServiceRoleF5508E89", + "Arn" + ] + }, + "Runtime": "nodejs22.x", + "Timeout": 900 + }, + "DependsOn": [ + "ModelsApiModelApiKeyCleanupProviderframeworkonEventServiceRoleDefaultPolicy13D5D897", + "ModelsApiModelApiKeyCleanupProviderframeworkonEventServiceRoleF5508E89" + ] + }, + "ModelsApiModelApiKeyCleanupResource4C024102": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "ModelsApiModelApiKeyCleanupProviderframeworkonEvent93A3C88F", + "Arn" + ] + }, + "CleanupVersion": "1" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": { + "Type": "AWS::IAM::Role", + "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" + ] + ] + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::cdk-hnb659fds-assets-012345678901-us-iso-east-1" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::cdk-hnb659fds-assets-012345678901-us-iso-east-1/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketA3074A95", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2bucketA3074A95", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "Roles": [ + { + "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9.zip" + }, + "Environment": { + "Variables": { + "AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "ModelsApidockerimagebuilderLisaModelsdockerimagebuilderec2dplmntAwsCliLayerB03BDD23" + } + ], + "Role": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + "Arn" + ] + }, + "Runtime": "python3.13", + "Timeout": 900 + }, + "DependsOn": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + ] + } + }, + "Parameters": { + "LisaRestApiUriStringParameterParameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/lisaServeRestApiUri" + }, + "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/layerVersion/common" + }, + "SsmParameterValuedevtestlisalisalayerVersionfastapiC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/layerVersion/fastapi" + }, + "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/bucket/bucket-access-logs" + }, + "SsmParameterValuedevtestlisalisamanagementKeySecretNameC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/managementKeySecretName" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "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/test/cdk/stacks/__baselines__/LisaNetworking.json b/test/cdk/stacks/__baselines__/LisaNetworking.json new file mode 100644 index 000000000..ac2bc6f2a --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaNetworking.json @@ -0,0 +1,717 @@ +{ + "Resources": { + "VpcVPC8B8C4E4B": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/22", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "LISA-VPC" + } + ] + } + }, + "VpcVPCpublicSubnet1SubnetA91B7DBE": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "dummy1a", + "CidrBlock": "10.0.0.0/26", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/publicSubnet1" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCpublicSubnet1RouteTableA07850BA": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/publicSubnet1" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCpublicSubnet1RouteTableAssociation48205878": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcVPCpublicSubnet1RouteTableA07850BA" + }, + "SubnetId": { + "Ref": "VpcVPCpublicSubnet1SubnetA91B7DBE" + } + } + }, + "VpcVPCpublicSubnet1DefaultRoute498D45E6": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcVPCIGWAC9DFBD8" + }, + "RouteTableId": { + "Ref": "VpcVPCpublicSubnet1RouteTableA07850BA" + } + }, + "DependsOn": [ + "VpcVPCVPCGWDD3D1AF6" + ] + }, + "VpcVPCpublicSubnet1EIP15896A4D": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/publicSubnet1" + } + ] + } + }, + "VpcVPCpublicSubnet1NATGatewayC3853FCB": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcVPCpublicSubnet1EIP15896A4D", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcVPCpublicSubnet1SubnetA91B7DBE" + }, + "Tags": [ + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/publicSubnet1" + } + ] + }, + "DependsOn": [ + "VpcVPCpublicSubnet1DefaultRoute498D45E6", + "VpcVPCpublicSubnet1RouteTableAssociation48205878" + ] + }, + "VpcVPCpublicSubnet2SubnetC9D5B981": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "dummy1b", + "CidrBlock": "10.0.0.64/26", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/publicSubnet2" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCpublicSubnet2RouteTable50B9E3ED": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/publicSubnet2" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCpublicSubnet2RouteTableAssociationA9980FE5": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcVPCpublicSubnet2RouteTable50B9E3ED" + }, + "SubnetId": { + "Ref": "VpcVPCpublicSubnet2SubnetC9D5B981" + } + } + }, + "VpcVPCpublicSubnet2DefaultRoute9995691F": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcVPCIGWAC9DFBD8" + }, + "RouteTableId": { + "Ref": "VpcVPCpublicSubnet2RouteTable50B9E3ED" + } + }, + "DependsOn": [ + "VpcVPCVPCGWDD3D1AF6" + ] + }, + "VpcVPCprivateIsolatedSubnet1Subnet595DCC9B": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "dummy1a", + "CidrBlock": "10.0.0.128/26", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "privateIsolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/privateIsolatedSubnet1" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCprivateIsolatedSubnet1RouteTable90B88BF9": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/privateIsolatedSubnet1" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCprivateIsolatedSubnet1RouteTableAssociationEE6A9AF3": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcVPCprivateIsolatedSubnet1RouteTable90B88BF9" + }, + "SubnetId": { + "Ref": "VpcVPCprivateIsolatedSubnet1Subnet595DCC9B" + } + } + }, + "VpcVPCprivateIsolatedSubnet2SubnetFD505B6C": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "dummy1b", + "CidrBlock": "10.0.0.192/26", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "privateIsolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/privateIsolatedSubnet2" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCprivateIsolatedSubnet2RouteTableDDB88BDF": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/privateIsolatedSubnet2" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCprivateIsolatedSubnet2RouteTableAssociation69FEEA26": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcVPCprivateIsolatedSubnet2RouteTableDDB88BDF" + }, + "SubnetId": { + "Ref": "VpcVPCprivateIsolatedSubnet2SubnetFD505B6C" + } + } + }, + "VpcVPCprivateSubnet1Subnet29B9FADC": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "dummy1a", + "CidrBlock": "10.0.1.0/26", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/privateSubnet1" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCprivateSubnet1RouteTableBD57150E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/privateSubnet1" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCprivateSubnet1RouteTableAssociationECDD1646": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcVPCprivateSubnet1RouteTableBD57150E" + }, + "SubnetId": { + "Ref": "VpcVPCprivateSubnet1Subnet29B9FADC" + } + } + }, + "VpcVPCprivateSubnet1DefaultRoute6B914B4C": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcVPCpublicSubnet1NATGatewayC3853FCB" + }, + "RouteTableId": { + "Ref": "VpcVPCprivateSubnet1RouteTableBD57150E" + } + } + }, + "VpcVPCprivateSubnet2Subnet63498DC1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "dummy1b", + "CidrBlock": "10.0.1.64/26", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/privateSubnet2" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCprivateSubnet2RouteTable364CB60F": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "LisaNetworking/Vpc/VPC/privateSubnet2" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCprivateSubnet2RouteTableAssociationE88BBAEC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcVPCprivateSubnet2RouteTable364CB60F" + }, + "SubnetId": { + "Ref": "VpcVPCprivateSubnet2Subnet63498DC1" + } + } + }, + "VpcVPCprivateSubnet2DefaultRoute4FF71C2C": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcVPCpublicSubnet1NATGatewayC3853FCB" + }, + "RouteTableId": { + "Ref": "VpcVPCprivateSubnet2RouteTable364CB60F" + } + } + }, + "VpcVPCIGWAC9DFBD8": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "LISA-VPC" + } + ] + } + }, + "VpcVPCVPCGWDD3D1AF6": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "InternetGatewayId": { + "Ref": "VpcVPCIGWAC9DFBD8" + }, + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcVPCS3GatewayEndpointBC0589FD": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "RouteTableIds": [ + { + "Ref": "VpcVPCprivateSubnet1RouteTableBD57150E" + }, + { + "Ref": "VpcVPCprivateSubnet2RouteTable364CB60F" + }, + { + "Ref": "VpcVPCpublicSubnet1RouteTableA07850BA" + }, + { + "Ref": "VpcVPCpublicSubnet2RouteTable50B9E3ED" + }, + { + "Ref": "VpcVPCprivateIsolatedSubnet1RouteTable90B88BF9" + }, + { + "Ref": "VpcVPCprivateIsolatedSubnet2RouteTableDDB88BDF" + } + ], + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "LISA-VPC" + } + ], + "VpcEndpointType": "Gateway", + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcEcsModelAlbSg5FC4C18E": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for ECS model application load balancer", + "GroupName": "test-lisa-ECS-ALB-SG", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": { + "Fn::GetAtt": [ + "VpcVPC8B8C4E4B", + "CidrBlock" + ] + }, + "Description": "Allow VPC traffic on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow any traffic on port 443", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcRestApiAlbSg469AE4F8": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for REST API application load balancer", + "GroupName": "test-lisa-RestAPI-ALB-SG", + "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 + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + }, + "VpcLambdaSecurityGroup184B54BD": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for authorizer and API Lambdas", + "GroupName": "test-lisa-Lambda-SG", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "VpcVPC8B8C4E4B" + } + } + } + }, + "Outputs": { + "VpcvpcArn6B7D14FD": { + "Value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:us-iso-east-1:012345678901:vpc/", + { + "Ref": "VpcVPC8B8C4E4B" + } + ] + ] + } + }, + "VpcvpcCidrBlockC554E24E": { + "Value": { + "Fn::GetAtt": [ + "VpcVPC8B8C4E4B", + "CidrBlock" + ] + } + }, + "VpcecsModelAlbSg30A66E62": { + "Value": { + "Fn::GetAtt": [ + "VpcEcsModelAlbSg5FC4C18E", + "GroupId" + ] + } + }, + "VpcrestApiAlbSgA687ACD7": { + "Value": { + "Fn::GetAtt": [ + "VpcRestApiAlbSg469AE4F8", + "GroupId" + ] + } + }, + "VpclambdaSecurityGroup0CD8914A": { + "Value": { + "Fn::GetAtt": [ + "VpcLambdaSecurityGroup184B54BD", + "GroupId" + ] + } + }, + "ExportsOutputRefVpcVPC8B8C4E4BB8544CDA": { + "Value": { + "Ref": "VpcVPC8B8C4E4B" + }, + "Export": { + "Name": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + }, + "ExportsOutputFnGetAttVpcRestApiAlbSg469AE4F8GroupIdDB418565": { + "Value": { + "Fn::GetAtt": [ + "VpcRestApiAlbSg469AE4F8", + "GroupId" + ] + }, + "Export": { + "Name": "LisaNetworking:ExportsOutputFnGetAttVpcRestApiAlbSg469AE4F8GroupIdDB418565" + } + }, + "ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F": { + "Value": { + "Ref": "VpcVPCprivateSubnet1Subnet29B9FADC" + }, + "Export": { + "Name": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + } + }, + "ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD": { + "Value": { + "Ref": "VpcVPCprivateSubnet2Subnet63498DC1" + }, + "Export": { + "Name": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + }, + "ExportsOutputRefVpcVPCpublicSubnet1SubnetA91B7DBE8B8D2123": { + "Value": { + "Ref": "VpcVPCpublicSubnet1SubnetA91B7DBE" + }, + "Export": { + "Name": "LisaNetworking:ExportsOutputRefVpcVPCpublicSubnet1SubnetA91B7DBE8B8D2123" + } + }, + "ExportsOutputRefVpcVPCpublicSubnet2SubnetC9D5B981D613E068": { + "Value": { + "Ref": "VpcVPCpublicSubnet2SubnetC9D5B981" + }, + "Export": { + "Name": "LisaNetworking:ExportsOutputRefVpcVPCpublicSubnet2SubnetC9D5B981D613E068" + } + }, + "ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB": { + "Value": { + "Fn::GetAtt": [ + "VpcLambdaSecurityGroup184B54BD", + "GroupId" + ] + }, + "Export": { + "Name": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + }, + "ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A": { + "Value": { + "Fn::GetAtt": [ + "VpcEcsModelAlbSg5FC4C18E", + "GroupId" + ] + }, + "Export": { + "Name": "LisaNetworking:ExportsOutputFnGetAttVpcEcsModelAlbSg5FC4C18EGroupId3AE6D77A" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "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/test/cdk/stacks/__baselines__/LisaRAG.json b/test/cdk/stacks/__baselines__/LisaRAG.json new file mode 100644 index 000000000..ca4359eb4 --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaRAG.json @@ -0,0 +1,6360 @@ +{ + "Parameters": { + "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/modelTableName" + }, + "LisaRestApiUriStringParameterParameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/lisaServeRestApiUri" + }, + "RegisteredModelsStringParameterParameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/registeredModels" + }, + "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/bucket/bucket-access-logs" + }, + "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/roles/test-lisa-LisaRagLambdaExecutionRole" + }, + "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/layerVersion/common" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Resources": { + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + "/*" + ] + ] + } + }, + { + "Action": "s3:DeleteObject*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + "/*" + ] + ] + } + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobTable8103D8C1", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobTable8103D8C1", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": "batch:SubmitJob", + "Effect": "Allow", + "Resource": [ + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + }, + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + }, + { + "Action": "batch:SubmitJob", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:us-iso-east-1:012345678901:parameter/dev/test-lisa/lisa/registeredModels" + ] + ] + } + }, + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:us-iso-east-1:012345678901:parameter/dev/test-lisa/lisa/lisaServeRestApiUri" + ] + ] + } + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlisaRagDocumentTable5A134785", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testlisaRagDocumentTable5A134785", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlisaRagSubDocumentTable76E4AE52", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60", + "Roles": [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + "LISARAGtestlisadevFF387D45": { + "Type": "AWS::S3::Bucket", + "Properties": { + "CorsConfiguration": { + "CorsRules": [ + { + "AllowedHeaders": [ + "*" + ], + "AllowedMethods": [ + "GET", + "POST" + ], + "AllowedOrigins": [ + "*" + ], + "ExposedHeaders": [ + "Access-Control-Allow-Origin" + ] + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + }, + "LogFilePrefix": "logs/rag-bucket/" + }, + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LISARAGtestlisadevPolicyF173721C": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:PutBucketPolicy", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + } + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + "/*" + ] + ] + } + }, + { + "Action": "s3:DeleteObject*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + } + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LISARAGtestlisadevFF387D45", + "Arn" + ] + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + } + }, + "LISARAGtestlisadevAutoDeleteObjectsCustomResourceE4AE69D8": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "LISARAGtestlisadevFF387D45" + } + }, + "DependsOn": [ + "LISARAGtestlisadevPolicyF173721C" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": "nodejs22.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "LISARAGtestlisadevFF387D45" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "testlisaRagDocumentTable5A134785": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "pk", + "AttributeType": "S" + }, + { + "AttributeName": "document_id", + "AttributeType": "S" + }, + { + "AttributeName": "repository_id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "GlobalSecondaryIndexes": [ + { + "IndexName": "document_index", + "KeySchema": [ + { + "AttributeName": "document_id", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "repository_index", + "KeySchema": [ + { + "AttributeName": "repository_id", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "KeySchema": [ + { + "AttributeName": "pk", + "KeyType": "HASH" + }, + { + "AttributeName": "document_id", + "KeyType": "RANGE" + } + ], + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "testlisaRagSubDocumentTable76E4AE52": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "document_id", + "AttributeType": "S" + }, + { + "AttributeName": "sk", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "document_id", + "KeyType": "HASH" + }, + { + "AttributeName": "sk", + "KeyType": "RANGE" + } + ], + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "RagLayerF72F34A6": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "python3.11" + ], + "Content": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "bdb843ae57a1a6f5714c8910570451c0e9f842588047ce7f612cb88c83ff52ed.zip" + }, + "Description": "Lambda dependencies for RAG API" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "testlisadevRagLayerECB05657": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/layerVersion/rag", + "Type": "String", + "Value": { + "Ref": "RagLayerF72F34A6" + } + } + }, + "LISAOpenSearchSg9A9088E8": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for RAG OpenSearch domain", + "GroupName": "test-lisa-OpenSearch-SG", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "10.0.0.128/26", + "Description": "Allow REST API private subnets to communicate with LISA-OpenSearchSg", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + }, + { + "CidrIp": "10.0.0.192/26", + "Description": "Allow REST API private subnets to communicate with LISA-OpenSearchSg", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + }, + { + "CidrIp": "10.0.1.0/26", + "Description": "Allow REST API private subnets to communicate with LISA-OpenSearchSg", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + }, + { + "CidrIp": "10.0.1.64/26", + "Description": "Allow REST API private subnets to communicate with LISA-OpenSearchSg", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + }, + { + "CidrIp": "10.0.0.128/26", + "Description": "Allow REST API private subnets to communicate with LISA-OpenSearchSg", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + }, + { + "CidrIp": "10.0.0.192/26", + "Description": "Allow REST API private subnets to communicate with LISA-OpenSearchSg", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + }, + { + "CidrIp": "10.0.1.0/26", + "Description": "Allow REST API private subnets to communicate with LISA-OpenSearchSg", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + }, + { + "CidrIp": "10.0.1.64/26", + "Description": "Allow REST API private subnets to communicate with LISA-OpenSearchSg", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + } + ], + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "openSearchSecurityGroupIdStringParameter5B897A08": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Security Group ID for OpenSearch domain", + "Name": "/dev/test-lisa/lisa/openSearchSecurityGroupId", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "LISAOpenSearchSg9A9088E8", + "GroupId" + ] + } + } + }, + "LISAPGVectorSgA2425084": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for RAG PGVector database", + "GroupName": "LISA-PGVector-SG", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "10.0.0.128/26", + "Description": "Allow REST API private subnets to communicate with LISA-PGVectorSg", + "FromPort": 5432, + "IpProtocol": "tcp", + "ToPort": 5432 + }, + { + "CidrIp": "10.0.0.192/26", + "Description": "Allow REST API private subnets to communicate with LISA-PGVectorSg", + "FromPort": 5432, + "IpProtocol": "tcp", + "ToPort": 5432 + }, + { + "CidrIp": "10.0.1.0/26", + "Description": "Allow REST API private subnets to communicate with LISA-PGVectorSg", + "FromPort": 5432, + "IpProtocol": "tcp", + "ToPort": 5432 + }, + { + "CidrIp": "10.0.1.64/26", + "Description": "Allow REST API private subnets to communicate with LISA-PGVectorSg", + "FromPort": 5432, + "IpProtocol": "tcp", + "ToPort": 5432 + } + ], + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "pgvectorSecurityGroupIdStringParameterCCE24FA5": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Security Group ID for PGVector database", + "Name": "/dev/test-lisa/lisa/pgvectorSecurityGroupId", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "LISAPGVectorSgA2425084", + "GroupId" + ] + } + } + }, + "RagRepositoryConfigTable6FA366CB": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "repositoryId", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "repositoryId", + "KeyType": "HASH" + } + ], + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, + "SSESpecification": { + "SSEEnabled": true + }, + "StreamSpecification": { + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "TableName": "test-lisa-dev-rag-repository-config", + "TimeToLiveSpecification": { + "AttributeName": "ttl", + "Enabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "IngestionStackConstructIngestionJobTable8103D8C1": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + }, + { + "AttributeName": "document_id", + "AttributeType": "S" + }, + { + "AttributeName": "repository_id", + "AttributeType": "S" + }, + { + "AttributeName": "created_date", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "GlobalSecondaryIndexes": [ + { + "IndexName": "documentId", + "KeySchema": [ + { + "AttributeName": "document_id", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "repository_id-created_date-index", + "KeySchema": [ + { + "AttributeName": "repository_id", + "KeyType": "HASH" + }, + { + "AttributeName": "created_date", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "TableName": "test-lisa-dev-ingestion-job" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "IngestionStackConstructIngestionJobLogGroupED038CC2": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 7 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "IngestionStackConstructIngestionJobFargateEnvSecurityGroup2A62D5C0": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "LisaRAG/IngestionStackConstruct/IngestionJobFargateEnv/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "IngestionStackConstructIngestionJobFargateEnvD92342F8": { + "Type": "AWS::Batch::ComputeEnvironment", + "Properties": { + "ComputeEnvironmentName": "test-lisa-dev-ingestion-job-aa0aa494bf77", + "ComputeResources": { + "MaxvCpus": 128, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobFargateEnvSecurityGroup2A62D5C0", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ], + "Type": "FARGATE" + }, + "ReplaceComputeEnvironment": false, + "State": "ENABLED", + "Type": "managed", + "UpdatePolicy": {} + } + }, + "IngestionStackConstructIngestionJobQueueCECF0CDA": { + "Type": "AWS::Batch::JobQueue", + "Properties": { + "ComputeEnvironmentOrder": [ + { + "ComputeEnvironment": { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobFargateEnvD92342F8", + "ComputeEnvironmentArn" + ] + }, + "Order": 1 + } + ], + "JobQueueName": "test-lisa-dev-ingestion-job-aa0aa494bf77", + "Priority": 1, + "State": "ENABLED" + } + }, + "IngestionStackConstructIngestionJobContainerExecutionRole669198BD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "IngestionStackConstructIngestionJobContainerExecutionRoleDefaultPolicyB28C1B30": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobLogGroupED038CC2", + "Arn" + ] + } + }, + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:us-iso-east-1:012345678901:repository/cdk-hnb659fds-container-assets-012345678901-us-iso-east-1" + ] + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "IngestionStackConstructIngestionJobContainerExecutionRoleDefaultPolicyB28C1B30", + "Roles": [ + { + "Ref": "IngestionStackConstructIngestionJobContainerExecutionRole669198BD" + } + ] + } + }, + "IngestionStackConstructIngestionJobDefinition529FE179": { + "Type": "AWS::Batch::JobDefinition", + "Properties": { + "ContainerProperties": { + "Command": [ + "-m", + "repository.pipeline_ingestion", + "Ref::ACTION", + "Ref::DOCUMENT_ID" + ], + "Environment": [ + { + "Name": "ADMIN_GROUP", + "Value": "" + }, + { + "Name": "BUCKET_NAME", + "Value": { + "Ref": "LISARAGtestlisadevFF387D45" + } + }, + { + "Name": "CHUNK_OVERLAP", + "Value": "51" + }, + { + "Name": "CHUNK_SIZE", + "Value": "512" + }, + { + "Name": "LISA_API_URL_PS_NAME", + "Value": "/dev/test-lisa/lisa/lisaServeRestApiUri" + }, + { + "Name": "LOG_LEVEL", + "Value": "DEBUG" + }, + { + "Name": "MANAGEMENT_KEY_SECRET_NAME_PS", + "Value": "/dev/test-lisa/lisa/managementKeySecretName" + }, + { + "Name": "MODEL_TABLE_NAME", + "Value": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + } + }, + { + "Name": "RAG_DOCUMENT_TABLE", + "Value": { + "Ref": "testlisaRagDocumentTable5A134785" + } + }, + { + "Name": "RAG_SUB_DOCUMENT_TABLE", + "Value": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + } + }, + { + "Name": "REGISTERED_MODELS_PS_NAME", + "Value": "/dev/test-lisa/lisa/registeredModels" + }, + { + "Name": "REGISTERED_REPOSITORIES_PS_PREFIX", + "Value": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/" + }, + { + "Name": "REGISTERED_REPOSITORIES_PS", + "Value": "/dev/test-lisa/lisa/registeredRepositories" + }, + { + "Name": "REST_API_VERSION", + "Value": "v2" + }, + { + "Name": "TIKTOKEN_CACHE_DIR", + "Value": "/opt/python/TIKTOKEN_CACHE" + }, + { + "Name": "RESTAPI_SSL_CERT_ARN", + "Value": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev" + }, + { + "Name": "LISA_RAG_VECTOR_STORE_TABLE", + "Value": { + "Ref": "RagRepositoryConfigTable6FA366CB" + } + }, + { + "Name": "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER", + "Value": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create" + }, + { + "Name": "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER", + "Value": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete" + }, + { + "Name": "LISA_INGESTION_JOB_TABLE_NAME", + "Value": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + } + }, + { + "Name": "LISA_INGESTION_JOB_QUEUE_NAME", + "Value": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + } + } + ], + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobContainerExecutionRole669198BD", + "Arn" + ] + }, + "FargatePlatformConfiguration": {}, + "Image": { + "Fn::Sub": "012345678901.dkr.ecr.us-iso-east-1.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-012345678901-us-iso-east-1:cdf42103df5398f35f92245df3575c31893f829ef0f71d4ea87c5d37103ad3e5" + }, + "JobRoleArn": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "IngestionStackConstructIngestionJobLogGroupED038CC2" + }, + "awslogs-stream-prefix": "batch-job", + "awslogs-region": "us-iso-east-1" + } + }, + "NetworkConfiguration": { + "AssignPublicIp": "DISABLED" + }, + "ReadonlyRootFilesystem": false, + "ResourceRequirements": [ + { + "Type": "MEMORY", + "Value": "4096" + }, + { + "Type": "VCPU", + "Value": "2" + } + ], + "RuntimePlatform": {} + }, + "JobDefinitionName": "test-lisa-dev-ingestion-job-aa0aa494bf77", + "PlatformCapabilities": [ + "FARGATE" + ], + "RetryStrategy": { + "Attempts": 1 + }, + "Timeout": { + "AttemptDurationSeconds": 14400 + }, + "Type": "container" + } + }, + "IngestionStackConstructhandlePipelineIngestScheduleSecurityGroup0CE86B5C": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatic security group for Lambda Function LisaRAGIngestionStackConstructhandlePipelineIngestScheduleCD7DF5A4", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "IngestionStackConstructhandlePipelineIngestSchedule4418AB88": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "test-lisa-dev-ingestion-ingest-schedule-aa0aa494bf77", + "Handler": "repository.pipeline_ingest_documents.handle_pipline_ingest_schedule", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 256, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "IngestionStackConstructhandlePipelineIngestScheduleSecurityGroup0CE86B5C", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "IngestionStackConstructhandlePipelineIngestScheduleAllowEventBridgeInvoke333FA84B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "IngestionStackConstructhandlePipelineIngestSchedule4418AB88", + "Arn" + ] + }, + "Principal": "events.amazonaws.com" + } + }, + "IngestionStackConstructIngestionJobScheduleLambdaArn95353A02": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/ingestion/ingest/schedule", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "IngestionStackConstructhandlePipelineIngestSchedule4418AB88", + "Arn" + ] + } + } + }, + "IngestionStackConstructhandlePipelineIngestEventSecurityGroup0E4AA865": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatic security group for Lambda Function LisaRAGIngestionStackConstructhandlePipelineIngestEventE78BDF9C", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "IngestionStackConstructhandlePipelineIngestEvent30BCC23B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "test-lisa-dev-ingestion-ingest-event-aa0aa494bf77", + "Handler": "repository.pipeline_ingest_documents.handle_pipeline_ingest_event", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 256, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "IngestionStackConstructhandlePipelineIngestEventSecurityGroup0E4AA865", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "IngestionStackConstructhandlePipelineIngestEventAllowEventBridgeInvoke378D27AA": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "IngestionStackConstructhandlePipelineIngestEvent30BCC23B", + "Arn" + ] + }, + "Principal": "events.amazonaws.com" + } + }, + "IngestionStackConstructIngestionJobEventLambdaArn86A07D4D": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/ingestion/ingest/event", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "IngestionStackConstructhandlePipelineIngestEvent30BCC23B", + "Arn" + ] + } + } + }, + "IngestionStackConstructhandlePipelineDeleteEventSecurityGroup94D216B4": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatic security group for Lambda Function LisaRAGIngestionStackConstructhandlePipelineDeleteEventC539F9F0", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "IngestionStackConstructhandlePipelineDeleteEventC723B80D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "test-lisa-dev-ingestion-delete-event-aa0aa494bf77", + "Handler": "repository.pipeline_ingest_documents.handle_pipeline_delete_event", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 256, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 60, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "IngestionStackConstructhandlePipelineDeleteEventSecurityGroup94D216B4", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "IngestionStackConstructhandlePipelineDeleteEventAllowEventBridgeInvoke6FF761E2": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "IngestionStackConstructhandlePipelineDeleteEventC723B80D", + "Arn" + ] + }, + "Principal": "events.amazonaws.com" + } + }, + "IngestionStackConstructDeletionJobEventLambdaArnD1BD729F": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/ingestion/delete/event", + "Type": "String", + "Value": { + "Fn::GetAtt": [ + "IngestionStackConstructhandlePipelineDeleteEventC723B80D", + "Arn" + ] + } + } + }, + "VectorStoreCreatorStackdevtestlisalisarolestestlisaVectorStoreCreatorRole34491B98": { + "Type": "AWS::IAM::Role", + "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" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AWSCloudFormationFullAccess" + ] + ] + } + ] + } + }, + "VectorStoreCreatorStackdevtestlisalisarolestestlisaVectorStoreCreatorRoleDefaultPolicy2CAE92FD": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:*", + "ec2:*", + "rds:*", + "opensearch:*", + "ssm:*" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:CreateServiceLinkedRole", + "Condition": { + "StringEquals": { + "iam:AWSServiceName": [ + "opensearchservice.amazonaws.com", + "rds.amazonaws.com" + ] + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "iam:CreateRole", + "iam:DeleteRole", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:DeleteRolePolicy", + "iam:TagRole", + "iam:UntagRole", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + "iam:ListRoleTags", + "iam:UpdateAssumeRolePolicy", + "iam:ListRoles" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:AssumeRole", + "Effect": "Allow", + "Resource": [ + "arn:aws:iam::012345678901:role/cdk-*-deploy-role-012345678901-us-iso-east-1", + "arn:aws:iam::012345678901:role/cdk-hnb659fds-deploy-role-012345678901-us-iso-east-1" + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringEquals": { + "iam:PassedToService": [ + "cloudformation.amazonaws.com", + "lambda.amazonaws.com", + "events.amazonaws.com" + ] + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "VectorStoreCreatorStackdevtestlisalisarolestestlisaVectorStoreCreatorRoleDefaultPolicy2CAE92FD", + "Roles": [ + { + "Ref": "VectorStoreCreatorStackdevtestlisalisarolestestlisaVectorStoreCreatorRole34491B98" + } + ] + } + }, + "VectorStoreCreatorStacktestlisadevStateMachineRoleC93BE6D8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "VectorStoreCreatorStacktestlisadevStateMachineRoleDefaultPolicyDF7CE587": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "VectorStoreCreatorStacktestlisadevvectorstoredeployerFnB20C2A9E", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "VectorStoreCreatorStacktestlisadevvectorstoredeployerFnB20C2A9E", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VectorStoreCreatorStacktestlisadevvectorstoredeployerFnB20C2A9E", + "Arn" + ] + } + }, + { + "Action": [ + "cloudformation:DescribeStacks", + "cloudformation:DeleteStack" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:GetItem", + "dynamodb:DeleteItem" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + }, + { + "Action": "dynamodb:PutItem", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:us-iso-east-1:012345678901:table/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + } + ] + ] + } + }, + { + "Action": "cloudformation:describeStacks", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "dynamodb:UpdateItem", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:us-iso-east-1:012345678901:table/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + } + ] + ] + } + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "VectorStoreCreatorStackDeleteStoreStateMachineCleanupRepositoryDocsFuncB129B35B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "VectorStoreCreatorStackDeleteStoreStateMachineCleanupRepositoryDocsFuncB129B35B", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "dynamodb:GetItem", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:us-iso-east-1:012345678901:table/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + } + ] + ] + } + }, + { + "Action": "cloudformation:deleteStack", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "dynamodb:DeleteItem", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:us-iso-east-1:012345678901:table/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "VectorStoreCreatorStacktestlisadevStateMachineRoleDefaultPolicyDF7CE587", + "Roles": [ + { + "Ref": "VectorStoreCreatorStacktestlisadevStateMachineRoleC93BE6D8" + } + ] + } + }, + "VectorStoreCreatorStacktestlisadevvectorstoredeployerFnB20C2A9E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "a48910598306b50a312adfedfdd77008fb84c9e18795e4a8d52a7c012e216490.zip" + }, + "Environment": { + "Variables": { + "LISA_CONFIG": { + "Fn::Join": [ + "", + [ + "{\"accountNumber\":\"012345678901\",\"appName\":\"lisa\",\"deploymentName\":\"test-lisa\",\"deploymentStage\":\"dev\",\"deploymentPrefix\":\"/dev/test-lisa/lisa\",\"partition\":\"aws\",\"region\":\"us-iso-east-1\",\"removalPolicy\":\"destroy\",\"profile\":\"\",\"vpcId\":\"", + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + }, + "\"}" + ] + ] + }, + "LISA_RAG_VECTOR_STORE_TABLE": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + } + } + }, + "EphemeralStorage": { + "Size": 2048 + }, + "FunctionName": "test-lisa-dev-vector_store_deployer-Fn", + "Handler": "index.handler", + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "VectorStoreCreatorStackdevtestlisalisarolestestlisaVectorStoreCreatorRole34491B98", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 900, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "VectorStoreCreatorStackdevtestlisalisarolestestlisaVectorStoreCreatorRoleDefaultPolicy2CAE92FD", + "VectorStoreCreatorStackdevtestlisalisarolestestlisaVectorStoreCreatorRole34491B98" + ] + }, + "VectorStoreCreatorStackCreateVectorStoreStateMachineCreateStoreStateMachine6CB371E8": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"CreateVectorStoreEntry\",\"States\":{\"CreateVectorStoreEntry\":{\"Next\":\"CreateVectorStoreInfraChoice\",\"Type\":\"Task\",\"ResultPath\":\"$.dynamoResult\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::dynamodb:putItem\",\"Parameters\":{\"Item\":{\"repositoryId\":{\"S.$\":\"$.body.ragConfig.repositoryId\"},\"status\":{\"S\":\"CREATE_IN_PROGRESS\"},\"config\":{\"M.$\":\"$.config\"}},\"TableName\":\"", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "\"}},\"CreateVectorStoreInfraChoice\":{\"Type\":\"Choice\",\"Choices\":[{\"And\":[{\"Variable\":\"$.body.ragConfig.type\",\"StringEquals\":\"bedrock_knowledge_base\"},{\"Variable\":\"$.body.ragConfig.pipelines[0]\",\"IsPresent\":false}],\"Next\":\"UpdateBedrockKBSuccess\"}],\"Default\":\"DeployVectorStore\"},\"DeployVectorStore\":{\"Next\":\"DescribeStack\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Catch\":[{\"ErrorEquals\":[\"States.ALL\"],\"Next\":\"UpdateFailureStatus\"}],\"Type\":\"Task\",\"ResultPath\":\"$.deployResult\",\"ResultSelector\":{\"stackName.$\":\"$.Payload.stackName\"},\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "VectorStoreCreatorStacktestlisadevvectorstoredeployerFnB20C2A9E", + "Arn" + ] + }, + "\",\"Payload\":{\"ragConfig.$\":\"$.body.ragConfig\"}}},\"DescribeStack\":{\"Next\":\"DeploymentComplete?\",\"Type\":\"Task\",\"ResultPath\":\"$.deployResult\",\"ResultSelector\":{\"stackName.$\":\"$.Stacks[0].StackName\",\"status.$\":\"$.Stacks[0].StackStatus\"},\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::aws-sdk:cloudformation:describeStacks\",\"Parameters\":{\"StackName.$\":\"$.deployResult.stackName\"}},\"Wait\":{\"Type\":\"Wait\",\"Seconds\":30,\"Next\":\"DescribeStack\"},\"DeploymentComplete?\":{\"Type\":\"Choice\",\"Choices\":[{\"And\":[{\"Variable\":\"$.deployResult.status\",\"IsPresent\":true},{\"Or\":[{\"Variable\":\"$.deployResult.status\",\"StringEquals\":\"CREATE_IN_PROGRESS\"},{\"Variable\":\"$.deployResult.status\",\"StringEquals\":\"UPDATE_IN_PROGRESS\"},{\"Variable\":\"$.deployResult.status\",\"StringEquals\":\"UPDATE_COMPLETE_CLEANUP_IN_PROGRESS\"}]}],\"Next\":\"Wait\"},{\"And\":[{\"Variable\":\"$.deployResult.status\",\"IsPresent\":true},{\"Or\":[{\"Variable\":\"$.deployResult.status\",\"StringEquals\":\"CREATE_COMPLETE\"},{\"Variable\":\"$.deployResult.status\",\"StringEquals\":\"UPDATE_COMPLETE\"}]}],\"Next\":\"UpdateSuccessStatus\"}],\"Default\":\"UpdateFailureStatus\"},\"UpdateFailureStatus\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::dynamodb:updateItem\",\"Parameters\":{\"Key\":{\"repositoryId\":{\"S.$\":\"$.body.ragConfig.repositoryId\"}},\"TableName\":\"", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "\",\"ExpressionAttributeNames\":{\"#status\":\"status\",\"#stackName\":\"stackName\"},\"ExpressionAttributeValues\":{\":status\":{\"S\":\"CREATE_FAILED\"},\":stackName\":{\"S.$\":\"$.deployResult.stackName\"}},\"UpdateExpression\":\"SET #status = :status, #stackName = :stackName\"}},\"UpdateSuccessStatus\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::dynamodb:updateItem\",\"Parameters\":{\"Key\":{\"repositoryId\":{\"S.$\":\"$.body.ragConfig.repositoryId\"}},\"TableName\":\"", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "\",\"ExpressionAttributeNames\":{\"#status\":\"status\",\"#stackName\":\"stackName\"},\"ExpressionAttributeValues\":{\":status\":{\"S\":\"CREATE_COMPLETE\"},\":stackName\":{\"S.$\":\"$.deployResult.stackName\"}},\"UpdateExpression\":\"SET #status = :status, #stackName = :stackName\"}},\"UpdateBedrockKBSuccess\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::dynamodb:updateItem\",\"Parameters\":{\"Key\":{\"repositoryId\":{\"S.$\":\"$.body.ragConfig.repositoryId\"}},\"TableName\":\"", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "\",\"ExpressionAttributeNames\":{\"#status\":\"status\"},\"ExpressionAttributeValues\":{\":status\":{\"S\":\"CREATE_COMPLETE\"}},\"UpdateExpression\":\"SET #status = :status\"}}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "VectorStoreCreatorStacktestlisadevStateMachineRoleC93BE6D8", + "Arn" + ] + }, + "StateMachineType": "STANDARD" + }, + "DependsOn": [ + "VectorStoreCreatorStacktestlisadevStateMachineRoleDefaultPolicyDF7CE587", + "VectorStoreCreatorStacktestlisadevStateMachineRoleC93BE6D8" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "VectorStoreCreatorStackCreateVectorStoreStateMachineCreateStoreStateMachineArnBF39408A": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "Type": "String", + "Value": { + "Ref": "VectorStoreCreatorStackCreateVectorStoreStateMachineCreateStoreStateMachine6CB371E8" + } + } + }, + "VectorStoreCreatorStackCreateVectorStoreStateMachineStateMachineExecutePolicyC29C6010": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "VectorStoreCreatorStackCreateVectorStoreStateMachineCreateStoreStateMachine6CB371E8" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RagVectorStoreStateMacineCreateExec", + "Roles": [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + "VectorStoreCreatorStackDeleteStoreStateMachineCleanupRepositoryDocsFuncSecurityGroup12118FB8": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatic security group for Lambda Function LisaRAGVectorStoreCreatorStackDeleteStoreStateMachineCleanupRepositoryDocsFunc9BCC544B", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "VectorStoreCreatorStackDeleteStoreStateMachineCleanupRepositoryDocsFuncB129B35B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "Handler": "repository.state_machine.cleanup_repo_docs.lambda_handler", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 2048, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 900, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "VectorStoreCreatorStackDeleteStoreStateMachineCleanupRepositoryDocsFuncSecurityGroup12118FB8", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + } + }, + "VectorStoreCreatorStackDeleteStoreStateMachine70A7FD86": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"UpdateDeleteStatus\",\"States\":{\"UpdateDeleteStatus\":{\"Next\":\"ShouldSkipCleanup\",\"Type\":\"Task\",\"ResultPath\":\"$.updateDynamoDbResult\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::dynamodb:updateItem\",\"Parameters\":{\"Key\":{\"repositoryId\":{\"S.$\":\"$.repositoryId\"}},\"TableName\":\"", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "\",\"ExpressionAttributeNames\":{\"#status\":\"status\"},\"ExpressionAttributeValues\":{\":status\":{\"S\":\"DELETE_IN_PROGRESS\"}},\"UpdateExpression\":\"SET #status = :status\"}},\"ShouldSkipCleanup\":{\"Type\":\"Choice\",\"Choices\":[{\"And\":[{\"Variable\":\"$.skipDocumentRemoval\",\"IsPresent\":true},{\"Variable\":\"$.skipDocumentRemoval\",\"BooleanEquals\":true}],\"Next\":\"BedrockKnowledgeBase\"}],\"Default\":\"CleanupRepositoryDocs\"},\"CleanupRepositoryDocs\":{\"Next\":\"HasMoreDocs\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "VectorStoreCreatorStackDeleteStoreStateMachineCleanupRepositoryDocsFuncB129B35B", + "Arn" + ] + }, + "\",\"Payload\":{\"repositoryId.$\":\"$.repositoryId\",\"stackName.$\":\"$.stackName\"}}},\"HasMoreDocs\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.lastEvaluated\",\"IsNull\":false,\"Next\":\"CleanupRepositoryDocsRetry\"}],\"Default\":\"GetRepoFromDdb\"},\"GetRepoFromDdb\":{\"Next\":\"BedrockKnowledgeBase\",\"Type\":\"Task\",\"ResultPath\":\"$.ddbResult\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::dynamodb:getItem\",\"Parameters\":{\"Key\":{\"repositoryId\":{\"S.$\":\"$.repositoryId\"}},\"TableName\":\"", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "\",\"ConsistentRead\":false}},\"BedrockKnowledgeBase\":{\"Type\":\"Choice\",\"Choices\":[{\"And\":[{\"Variable\":\"$.ddbResult.Item.config.M.type.S\",\"StringEquals\":\"bedrock_knowledge_base\"},{\"Variable\":\"$.stackName\",\"IsNull\":true}],\"Next\":\"DeleteDynamoDbEntry\"}],\"Default\":\"DeleteStack\"},\"DeleteStack\":{\"Next\":\"Check Stack Status\",\"Type\":\"Task\",\"ResultPath\":\"$.deleteResult\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::aws-sdk:cloudformation:deleteStack\",\"Parameters\":{\"StackName.$\":\"$.stackName\"}},\"Check Stack Status\":{\"Next\":\"DeletionSuccessful?\",\"Catch\":[{\"ErrorEquals\":[\"States.ALL\"],\"ResultPath\":\"$.error\",\"Next\":\"DeleteDynamoDbEntry\"}],\"Type\":\"Task\",\"ResultPath\":\"$.checkResult\",\"ResultSelector\":{\"stackName.$\":\"$.Stacks[0].StackName\",\"status.$\":\"$.Stacks[0].StackStatus\"},\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::aws-sdk:cloudformation:describeStacks\",\"Parameters\":{\"StackName.$\":\"$.stackName\"}},\"Wait\":{\"Type\":\"Wait\",\"Seconds\":30,\"Next\":\"Check Stack Status\"},\"DeletionSuccessful?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.checkResult.status\",\"StringEquals\":\"DELETE_FAILED\",\"Next\":\"UpdateFailureStatus\"}],\"Default\":\"Wait\"},\"UpdateFailureStatus\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::dynamodb:updateItem\",\"Parameters\":{\"Key\":{\"id\":{\"S.$\":\"$.repositoryId\"}},\"TableName\":\"", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "\",\"ExpressionAttributeNames\":{\"#status\":\"status\",\"#error\":\"error\"},\"ExpressionAttributeValues\":{\":status\":{\"S\":\"$.checkResult.status\"}},\"UpdateExpression\":\"SET #status = :status, #error = :error\"}},\"DeleteDynamoDbEntry\":{\"End\":true,\"Type\":\"Task\",\"ResultPath\":\"$.deleteDynamoDbResult\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::dynamodb:deleteItem\",\"Parameters\":{\"Key\":{\"repositoryId\":{\"S.$\":\"$.repositoryId\"}},\"TableName\":\"", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "\"}},\"CleanupRepositoryDocsRetry\":{\"End\":true,\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "VectorStoreCreatorStackDeleteStoreStateMachineCleanupRepositoryDocsFuncB129B35B", + "Arn" + ] + }, + "\",\"Payload\":{\"repositoryId.$\":\"$.repositoryId\",\"lastEvaluated.$\":\"$.lastEvaluated\",\"stackName.$\":\"$.stackName\"}}}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "VectorStoreCreatorStacktestlisadevStateMachineRoleC93BE6D8", + "Arn" + ] + }, + "StateMachineType": "STANDARD" + }, + "DependsOn": [ + "VectorStoreCreatorStacktestlisadevStateMachineRoleDefaultPolicyDF7CE587", + "VectorStoreCreatorStacktestlisadevStateMachineRoleC93BE6D8" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "VectorStoreCreatorStackDeleteStoreStateMachinedevtestlisalisaVectorStoreCreatorDelete71860FC7": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "Type": "String", + "Value": { + "Ref": "VectorStoreCreatorStackDeleteStoreStateMachine70A7FD86" + } + } + }, + "VectorStoreCreatorStackDeleteStoreStateMachineStateMachineExecutePolicy17D4BD44": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "VectorStoreCreatorStackDeleteStoreStateMachine70A7FD86" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RagVectorStoreStateMachineDeleteExec", + "Roles": [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + "RepositoryApiRestApirepository40CDFF92": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "repository", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryOPTIONSEB79C10E": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepository40CDFF92" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryGETApiPermissionLisaRAGRepositoryApiRestApi0AD1924FGETrepositoryE35A7094": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorylistall7B1FB82C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/repository" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryGETApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FGETrepository8DD0591A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorylistall7B1FB82C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/repository" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryGETE71A6DCF": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorylistall7B1FB82C", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepository40CDFF92" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositorystatusD1253A39": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepository40CDFF92" + }, + "PathPart": "status", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositorystatusOPTIONS11CF4306": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositorystatusD1253A39" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositorystatusGETApiPermissionLisaRAGRepositoryApiRestApi0AD1924FGETrepositorystatusCE79B4BC": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositoryliststatus7E4D9E23", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/repository/status" + ] + ] + } + } + }, + "RepositoryApiRestApirepositorystatusGETApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FGETrepositorystatus9089F258": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositoryliststatus7E4D9E23", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/repository/status" + ] + ] + } + } + }, + "RepositoryApiRestApirepositorystatusGET3B80D731": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositoryliststatus7E4D9E23", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepositorystatusD1253A39" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositorypresignedurlE2569778": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepository40CDFF92" + }, + "PathPart": "presigned-url", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositorypresignedurlOPTIONS1F38581E": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositorypresignedurlE2569778" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositorypresignedurlPOSTApiPermissionLisaRAGRepositoryApiRestApi0AD1924FPOSTrepositorypresignedurl95C378D7": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorypresignedurlBBDFFDED", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/POST/repository/presigned-url" + ] + ] + } + } + }, + "RepositoryApiRestApirepositorypresignedurlPOSTApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FPOSTrepositorypresignedurlB6CF6C72": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorypresignedurlBBDFFDED", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/POST/repository/presigned-url" + ] + ] + } + } + }, + "RepositoryApiRestApirepositorypresignedurlPOST17EF9A79": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "POST", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorypresignedurlBBDFFDED", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepositorypresignedurlE2569778" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryPOSTApiPermissionLisaRAGRepositoryApiRestApi0AD1924FPOSTrepositoryBD24588A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorycreate82D293D2", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/POST/repository" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryPOSTApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FPOSTrepositoryD5697015": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorycreate82D293D2", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/POST/repository" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryPOSTA1F97EAB": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "POST", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorycreate82D293D2", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepository40CDFF92" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryId4FB9DAF5": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepository40CDFF92" + }, + "PathPart": "{repositoryId}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdOPTIONSE01318F5": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryId4FB9DAF5" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdDELETEApiPermissionLisaRAGRepositoryApiRestApi0AD1924FDELETErepositoryrepositoryId8CB5AA1E": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydelete061FD77D", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/DELETE/repository/*" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdDELETEApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FDELETErepositoryrepositoryId4070BF82": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydelete061FD77D", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/DELETE/repository/*" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdDELETE85130C2F": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "DELETE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydelete061FD77D", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryId4FB9DAF5" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdindex1CB71362": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryId4FB9DAF5" + }, + "PathPart": "index", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdindexOPTIONSAF471628": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIdindex1CB71362" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdindexmodelNameF5C83975": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIdindex1CB71362" + }, + "PathPart": "{modelName}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdindexmodelNameOPTIONSC4CEAE19": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIdindexmodelNameF5C83975" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdindexmodelNameDELETEApiPermissionLisaRAGRepositoryApiRestApi0AD1924FDELETErepositoryrepositoryIdindexmodelNameE30CA7DF": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydeleteindex018A1DE0", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/DELETE/repository/*/index/*" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdindexmodelNameDELETEApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FDELETErepositoryrepositoryIdindexmodelName8A911DBB": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydeleteindex018A1DE0", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/DELETE/repository/*/index/*" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdindexmodelNameDELETED7CAF702": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "DELETE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydeleteindex018A1DE0", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIdindexmodelNameF5C83975" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdsimilaritySearch878A5EDB": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryId4FB9DAF5" + }, + "PathPart": "similaritySearch", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdsimilaritySearchOPTIONSB527587F": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIdsimilaritySearch878A5EDB" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdsimilaritySearchGETApiPermissionLisaRAGRepositoryApiRestApi0AD1924FGETrepositoryrepositoryIdsimilaritySearch62BE8AAF": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorysimilaritysearch4A105094", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/repository/*/similaritySearch" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdsimilaritySearchGETApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FGETrepositoryrepositoryIdsimilaritySearch597BDB46": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorysimilaritysearch4A105094", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/repository/*/similaritySearch" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdsimilaritySearchGETD74E26BD": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorysimilaritysearch4A105094", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIdsimilaritySearch878A5EDB" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdbulkE48A131B": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryId4FB9DAF5" + }, + "PathPart": "bulk", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdbulkOPTIONS74A6CA91": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIdbulkE48A131B" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdbulkPOSTApiPermissionLisaRAGRepositoryApiRestApi0AD1924FPOSTrepositoryrepositoryIdbulk09DC3AD9": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositoryingestdocumentsD194F7BE", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/POST/repository/*/bulk" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdbulkPOSTApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FPOSTrepositoryrepositoryIdbulk7D8FEED7": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositoryingestdocumentsD194F7BE", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/POST/repository/*/bulk" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdbulkPOSTDAAF0143": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "POST", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositoryingestdocumentsD194F7BE", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIdbulkE48A131B" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentE33A2713": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryId4FB9DAF5" + }, + "PathPart": "document", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentOPTIONSAFF04374": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIddocumentE33A2713" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentGETApiPermissionLisaRAGRepositoryApiRestApi0AD1924FGETrepositoryrepositoryIddocument4F1802E4": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorylistdocs0127CF73", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/repository/*/document" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentGETApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FGETrepositoryrepositoryIddocumentDD70EEF0": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorylistdocs0127CF73", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/repository/*/document" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentGET7585AD1B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorylistdocs0127CF73", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIddocumentE33A2713" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentDELETEApiPermissionLisaRAGRepositoryApiRestApi0AD1924FDELETErepositoryrepositoryIddocumentA11EC982": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydeletedocuments4D39BB65", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/DELETE/repository/*/document" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentDELETEApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FDELETErepositoryrepositoryIddocument3149B8A9": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydeletedocuments4D39BB65", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/DELETE/repository/*/document" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentDELETE91841B98": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "DELETE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydeletedocuments4D39BB65", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIddocumentE33A2713" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentId9D9F1E60": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryId4FB9DAF5" + }, + "PathPart": "{documentId}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentIdOPTIONS1F7E9895": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIddocumentId9D9F1E60" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentIddownload4E8391B1": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIddocumentId9D9F1E60" + }, + "PathPart": "download", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentIddownloadOPTIONS7A545475": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIddocumentIddownload4E8391B1" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentIddownloadGETApiPermissionLisaRAGRepositoryApiRestApi0AD1924FGETrepositoryrepositoryIddocumentIddownloadD841D661": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydownloaddocument5BF87366", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/repository/*/*/download" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentIddownloadGETApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FGETrepositoryrepositoryIddocumentIddownload8C3C0799": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydownloaddocument5BF87366", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/repository/*/*/download" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIddocumentIddownloadGETF4309BA2": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorydownloaddocument5BF87366", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIddocumentIddownload4E8391B1" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdjobs47FDE3A4": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryId4FB9DAF5" + }, + "PathPart": "jobs", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdjobsOPTIONS1CC42C85": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ], + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIdjobs47FDE3A4" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdjobsGETApiPermissionLisaRAGRepositoryApiRestApi0AD1924FGETrepositoryrepositoryIdjobs578C606D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorylistjobs49FA647F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/*/GET/repository/*/jobs" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdjobsGETApiPermissionTestLisaRAGRepositoryApiRestApi0AD1924FGETrepositoryrepositoryIdjobs41A39759": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorylistjobs49FA647F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:us-iso-east-1:012345678901:", + { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + }, + "/test-invoke-stage/GET/repository/*/jobs" + ] + ] + } + } + }, + "RepositoryApiRestApirepositoryrepositoryIdjobsGET245EAB42": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiAuthorizerAPIGWAuthorizerE7001880E41EC3EB" + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "RepositoryApiLisaRAGrepositorylistjobs49FA647F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "ResourceId": { + "Ref": "RepositoryApiRestApirepositoryrepositoryIdjobs47FDE3A4" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RepositoryApiLisaRAGrepositorylistall7B1FB82C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "List all repositories", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-list_all", + "Handler": "repository.lambda_functions.list_all", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositoryliststatus7E4D9E23": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "List status for all repositories", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-list_status", + "Handler": "repository.lambda_functions.list_status", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositorypresignedurlBBDFFDED": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Generates a presigned url for uploading files to RAG", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-presigned_url", + "Handler": "repository.lambda_functions.presigned_url", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositorycreate82D293D2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Create a new repository", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-create", + "Handler": "repository.lambda_functions.create", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositorydelete061FD77D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Delete a repository", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-delete", + "Handler": "repository.lambda_functions.delete", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositorydeleteindex018A1DE0": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Delete an index within a repository", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-delete_index", + "Handler": "repository.lambda_functions.delete_index", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositorysimilaritysearch4A105094": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Run a similarity search against the specified repository using the specified query", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-similarity_search", + "Handler": "repository.lambda_functions.similarity_search", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositoryingestdocumentsD194F7BE": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Ingest a set of documents based on specified S3 path", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-ingest_documents", + "Handler": "repository.lambda_functions.ingest_documents", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 900, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositorylistdocs0127CF73": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "List all docs for a repository", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-list_docs", + "Handler": "repository.lambda_functions.list_docs", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositorydownloaddocument5BF87366": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Creates presigned url to download document within repository", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-download_document", + "Handler": "repository.lambda_functions.download_document", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositorydeletedocuments4D39BB65": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "Deletes all records associated with documents from the repository", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-delete_documents", + "Handler": "repository.lambda_functions.delete_documents", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + }, + "RepositoryApiLisaRAGrepositorylistjobs49FA647F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Description": "List all ingestion jobs for a repository", + "Environment": { + "Variables": { + "ADMIN_GROUP": "", + "BUCKET_NAME": { + "Ref": "LISARAGtestlisadevFF387D45" + }, + "CHUNK_OVERLAP": "51", + "CHUNK_SIZE": "512", + "LISA_API_URL_PS_NAME": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "LOG_LEVEL": "DEBUG", + "MANAGEMENT_KEY_SECRET_NAME_PS": "/dev/test-lisa/lisa/managementKeySecretName", + "MODEL_TABLE_NAME": { + "Ref": "LisaRAGResourcesModelTableNameStringParameterParameter9C4F30B2" + }, + "RAG_DOCUMENT_TABLE": { + "Ref": "testlisaRagDocumentTable5A134785" + }, + "RAG_SUB_DOCUMENT_TABLE": { + "Ref": "testlisaRagSubDocumentTable76E4AE52" + }, + "REGISTERED_MODELS_PS_NAME": "/dev/test-lisa/lisa/registeredModels", + "REGISTERED_REPOSITORIES_PS_PREFIX": "/dev/test-lisa/lisa/LisaServeRagConnectionInfo/", + "REGISTERED_REPOSITORIES_PS": "/dev/test-lisa/lisa/registeredRepositories", + "REST_API_VERSION": "v2", + "TIKTOKEN_CACHE_DIR": "/opt/python/TIKTOKEN_CACHE", + "RESTAPI_SSL_CERT_ARN": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev", + "LISA_RAG_VECTOR_STORE_TABLE": { + "Ref": "RagRepositoryConfigTable6FA366CB" + }, + "LISA_RAG_CREATE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/create", + "LISA_RAG_DELETE_STATE_MACHINE_ARN_PARAMETER": "/dev/test-lisa/lisa/vectorstorecreator/statemachine/delete", + "LISA_INGESTION_JOB_TABLE_NAME": { + "Ref": "IngestionStackConstructIngestionJobTable8103D8C1" + }, + "LISA_INGESTION_JOB_QUEUE_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "IngestionStackConstructIngestionJobQueueCECF0CDA", + "JobQueueArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + "LISA_INGESTION_JOB_DEFINITION_NAME": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "IngestionStackConstructIngestionJobDefinition529FE179" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "FunctionName": "LisaRAG-repository-list_jobs", + "Handler": "repository.lambda_functions.list_jobs", + "Layers": [ + { + "Ref": "RagLayerF72F34A6" + }, + { + "Ref": "SsmParameterValuedevtestlisalisalayerVersioncommonC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "MemorySize": 512, + "Role": { + "Ref": "SsmParameterValuedevtestlisalisarolestestlisaLisaRagLambdaExecutionRoleC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Runtime": "python3.11", + "Timeout": 180, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaRAGResourcesLisaRagLambdaExecutionRolePolicy1F0EBC60" + ] + } + }, + "Outputs": { + "ragVectorStoreTable": { + "Value": { + "Fn::GetAtt": [ + "RagRepositoryConfigTable6FA366CB", + "Arn" + ] + } + } + }, + "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/test/cdk/stacks/__baselines__/LisaServe.json b/test/cdk/stacks/__baselines__/LisaServe.json new file mode 100644 index 000000000..b363c4db9 --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaServe.json @@ -0,0 +1,2959 @@ +{ + "Transform": "AWS::SecretsManager-2024-09-16", + "Resources": { + "TokenTable3625D248": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "token", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "token", + "KeyType": "HASH" + } + ], + "SSESpecification": { + "SSEEnabled": true + }, + "TableName": "test-lisa-LISAApiTokenTable" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "RestApiECSClustertestlisadevClC04148B6": { + "Type": "AWS::ECS::Cluster", + "Properties": { + "ClusterName": "test-lisa-dev-REST", + "ClusterSettings": [ + { + "Name": "containerInsights", + "Value": "disabled" + } + ] + } + }, + "RestApiECSClustertestlisadevClF05BDFDE": { + "Type": "AWS::ECS::ClusterCapacityProviderAssociations", + "Properties": { + "CapacityProviders": [ + { + "Ref": "RestApiECSClustertestlisadevAsgCapacityProviderF18999A7" + } + ], + "Cluster": { + "Ref": "RestApiECSClustertestlisadevClC04148B6" + }, + "DefaultCapacityProviderStrategy": [] + } + }, + "RestApiECSClusterRestAsgSecurityGroup6583A565": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "LisaServe/RestApi-ECSCluster/RestAsgSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "RestApiECSClusterRestAsgSecurityGroupfromLisaNetworkingVpcRestApiAlbSgA916BD54ALLPORTS37A86A5D": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "Description": "from LisaNetworkingVpcRestApiAlbSgA916BD54:ALL PORTS", + "FromPort": 0, + "GroupId": { + "Fn::GetAtt": [ + "RestApiECSClusterRestAsgSecurityGroup6583A565", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcRestApiAlbSg469AE4F8GroupIdDB418565" + }, + "ToPort": 65535 + } + }, + "RestApiECSClustertestlisadevASGInstanceRole4F18B1DE": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/CloudWatchLogsFullAccess" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonSSMFullAccess" + ] + ] + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "LisaServe/RestApi-ECSCluster/test-lisa-dev-ASG" + } + ] + } + }, + "RestApiECSClustertestlisadevASGInstanceRoleDefaultPolicy763E7201": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:DeregisterContainerInstance", + "ecs:RegisterContainerInstance", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevClC04148B6", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevClC04148B6", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RestApiECSClustertestlisadevASGInstanceRoleDefaultPolicy763E7201", + "Roles": [ + { + "Ref": "RestApiECSClustertestlisadevASGInstanceRole4F18B1DE" + } + ] + } + }, + "RestApiECSClustertestlisadevASGInstanceProfile5F0036EC": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "RestApiECSClustertestlisadevASGInstanceRole4F18B1DE" + } + ] + } + }, + "RestApiECSClustertestlisadevASGLaunchConfigD5B6F73C": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "Encrypted": true, + "VolumeSize": 50 + } + } + ], + "IamInstanceProfile": { + "Ref": "RestApiECSClustertestlisadevASGInstanceProfile5F0036EC" + }, + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceMonitoring": true, + "InstanceType": "m5.large", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "RestApiECSClusterRestAsgSecurityGroup6583A565", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "RestApiECSClustertestlisadevClC04148B6" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" + ] + ] + } + } + }, + "DependsOn": [ + "RestApiECSClustertestlisadevASGInstanceRoleDefaultPolicy763E7201", + "RestApiECSClustertestlisadevASGInstanceRole4F18B1DE" + ] + }, + "RestApiECSClustertestlisadevASGCAE610D7": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "AutoScalingGroupName": "test-lisa-dev-REST", + "Cooldown": "60", + "DefaultInstanceWarmup": 60, + "LaunchConfigurationName": { + "Ref": "RestApiECSClustertestlisadevASGLaunchConfigD5B6F73C" + }, + "MaxSize": "5", + "MetricsCollection": [ + { + "Granularity": "1Minute" + } + ], + "MinSize": "1", + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "LisaServe/RestApi-ECSCluster/test-lisa-dev-ASG" + } + ], + "VPCZoneIdentifier": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + }, + "UpdatePolicy": { + "AutoScalingRollingUpdate": { + "SuspendProcesses": [ + "HealthCheck", + "ReplaceUnhealthy", + "AZRebalance", + "AlarmNotification", + "ScheduledActions", + "InstanceRefresh" + ] + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRole63B8BEE5": { + "Type": "AWS::IAM::Role", + "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": "Name", + "Value": "LisaServe/RestApi-ECSCluster/test-lisa-dev-ASG" + } + ] + } + }, + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRoleDefaultPolicy95F8B781": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:DescribeInstances", + "ec2:DescribeInstanceAttribute", + "ec2:DescribeInstanceStatus", + "ec2:DescribeHosts" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "autoscaling:CompleteLifecycleAction", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":autoscaling:us-iso-east-1:012345678901:autoScalingGroup:*:autoScalingGroupName/", + { + "Ref": "RestApiECSClustertestlisadevASGCAE610D7" + } + ] + ] + } + }, + { + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevClC04148B6", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevClC04148B6", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevClC04148B6", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRoleDefaultPolicy95F8B781", + "Roles": [ + { + "Ref": "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRole63B8BEE5" + } + ] + } + }, + "RestApiECSClustertestlisadevASGDrainECSHookFunctionB9703624": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(dict(event, ResponseURL='...')))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print(f\"Got event without EC2InstanceId: { json.dumps(dict(event, ResponseURL='...')) }\")\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n task_arns = container_instance_task_arns(cluster, instance_arn)\n\n if task_arns:\n print('Instance ARN %s has task ARNs %s' % (instance_arn, ', '.join(task_arns)))\n\n while has_tasks(cluster, instance_arn, task_arns):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\ndef container_instance_task_arns(cluster, instance_arn):\n \"\"\"Fetch tasks for a container instance ARN.\"\"\"\n arns = ecs.list_tasks(cluster=cluster, containerInstance=instance_arn)['taskArns']\n return arns\n\ndef has_tasks(cluster, instance_arn, task_arns):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n task_count = None\n\n if task_arns:\n # Fetch details for tasks running on the container instance\n tasks = ecs.describe_tasks(cluster=cluster, tasks=task_arns)['tasks']\n if tasks:\n # Consider any non-stopped tasks as running\n task_count = sum(task['lastStatus'] != 'STOPPED' for task in tasks) + instance['pendingTasksCount']\n\n if not task_count:\n # Fallback to instance task counts if detailed task information is unavailable\n task_count = instance['runningTasksCount'] + instance['pendingTasksCount']\n\n print('Instance %s has %s tasks' % (instance_arn, task_count))\n\n return task_count > 0\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" + }, + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "RestApiECSClustertestlisadevClC04148B6" + } + } + }, + "Handler": "index.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRole63B8BEE5", + "Arn" + ] + }, + "Runtime": "python3.13", + "Tags": [ + { + "Key": "Name", + "Value": "LisaServe/RestApi-ECSCluster/test-lisa-dev-ASG" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRoleDefaultPolicy95F8B781", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRole63B8BEE5" + ] + }, + "RestApiECSClustertestlisadevASGDrainECSHookFunctionAllowInvokeLisaServeRestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic7E3AEC6718E71340": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevASGDrainECSHookFunctionB9703624", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E" + } + } + }, + "RestApiECSClustertestlisadevASGDrainECSHookFunctionTopicBAEEB019": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevASGDrainECSHookFunctionB9703624", + "Arn" + ] + }, + "Protocol": "lambda", + "TopicArn": { + "Ref": "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E" + } + } + }, + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "LisaServe/RestApi-ECSCluster/test-lisa-dev-ASG" + } + ] + } + }, + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleEA0CB372": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "autoscaling.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "LisaServe/RestApi-ECSCluster/test-lisa-dev-ASG" + } + ] + } + }, + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleDefaultPolicyEC0162A6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleDefaultPolicyEC0162A6", + "Roles": [ + { + "Ref": "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleEA0CB372" + } + ] + } + }, + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookAC6818CC": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "RestApiECSClustertestlisadevASGCAE610D7" + }, + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "NotificationTargetARN": { + "Ref": "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E" + }, + "RoleARN": { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleEA0CB372", + "Arn" + ] + } + }, + "DependsOn": [ + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleDefaultPolicyEC0162A6", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleEA0CB372" + ] + }, + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperPolicy4621F1CA": { + "Type": "AWS::AutoScaling::ScalingPolicy", + "Properties": { + "AdjustmentType": "ChangeInCapacity", + "AutoScalingGroupName": { + "Ref": "RestApiECSClustertestlisadevASGCAE610D7" + }, + "MetricAggregationType": "Average", + "PolicyType": "StepScaling", + "StepAdjustments": [ + { + "MetricIntervalLowerBound": 0, + "MetricIntervalUpperBound": 20, + "ScalingAdjustment": 2 + }, + { + "MetricIntervalLowerBound": 20, + "ScalingAdjustment": 1 + } + ] + } + }, + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperAlarmCA336253": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmActions": [ + { + "Ref": "RestApiECSClustertestlisadevASGASGRESTScaleInUpperPolicy4621F1CA" + } + ], + "AlarmDescription": "Upper threshold scaling alarm", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "CapacityProviderName", + "Value": { + "Ref": "RestApiECSClustertestlisadevAsgCapacityProviderF18999A7" + } + }, + { + "Name": "ClusterName", + "Value": { + "Ref": "RestApiECSClustertestlisadevClC04148B6" + } + } + ], + "EvaluationPeriods": 5, + "MetricName": "CapacityProviderReservation", + "Namespace": "AWS/ECS/CapacityProvider", + "Period": 60, + "Statistic": "Average", + "Tags": [ + { + "Key": "Name", + "Value": "LisaServe/RestApi-ECSCluster/test-lisa-dev-ASG" + } + ], + "Threshold": 40 + } + }, + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerPolicy2BDAE944": { + "Type": "AWS::AutoScaling::ScalingPolicy", + "Properties": { + "AdjustmentType": "ChangeInCapacity", + "AutoScalingGroupName": { + "Ref": "RestApiECSClustertestlisadevASGCAE610D7" + }, + "MetricAggregationType": "Average", + "PolicyType": "StepScaling", + "StepAdjustments": [ + { + "MetricIntervalLowerBound": -10, + "MetricIntervalUpperBound": 0, + "ScalingAdjustment": 2 + }, + { + "MetricIntervalUpperBound": -10, + "ScalingAdjustment": 1 + } + ] + } + }, + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerAlarm17D7099D": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmActions": [ + { + "Ref": "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerPolicy2BDAE944" + } + ], + "AlarmDescription": "Lower threshold scaling alarm", + "ComparisonOperator": "LessThanOrEqualToThreshold", + "Dimensions": [ + { + "Name": "CapacityProviderName", + "Value": { + "Ref": "RestApiECSClustertestlisadevAsgCapacityProviderF18999A7" + } + }, + { + "Name": "ClusterName", + "Value": { + "Ref": "RestApiECSClustertestlisadevClC04148B6" + } + } + ], + "EvaluationPeriods": 2, + "MetricName": "CapacityProviderReservation", + "Namespace": "AWS/ECS/CapacityProvider", + "Period": 60, + "Statistic": "Average", + "Tags": [ + { + "Key": "Name", + "Value": "LisaServe/RestApi-ECSCluster/test-lisa-dev-ASG" + } + ], + "Threshold": 90 + } + }, + "RestApiECSClustertestlisadevAsgCapacityProviderF18999A7": { + "Type": "AWS::ECS::CapacityProvider", + "Properties": { + "AutoScalingGroupProvider": { + "AutoScalingGroupArn": { + "Ref": "RestApiECSClustertestlisadevASGCAE610D7" + }, + "ManagedTerminationProtection": "DISABLED" + } + } + }, + "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/ecs/test-lisa-dev-REST", + "RetentionInDays": 7 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "RestApiECSClusterdevtestlisalisaRESTLogGroupPolicyResourcePolicy35CF92BF": { + "Type": "AWS::Logs::ResourcePolicy", + "Properties": { + "PolicyDocument": { + "Fn::Join": [ + "", + [ + "{\"Statement\":[{\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"", + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolesRESTC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + }, + "\"},\"Resource\":\"", + { + "Fn::GetAtt": [ + "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2", + "Arn" + ] + }, + "\"},{\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"", + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolesRESTEXC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + }, + "\"},\"Resource\":\"", + { + "Fn::GetAtt": [ + "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2", + "Arn" + ] + }, + "\"},{\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"", + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolesMCPWORKBENCHC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + }, + "\"},\"Resource\":\"", + { + "Fn::GetAtt": [ + "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2", + "Arn" + ] + }, + "\"},{\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"", + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolesMCPWORKBENCHEXC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + }, + "\"},\"Resource\":\"", + { + "Fn::GetAtt": [ + "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2", + "Arn" + ] + }, + "\"}],\"Version\":\"2012-10-17\"}" + ] + ] + }, + "PolicyName": "LisaServeRestApiECSClusterdevtestlisalisaRESTLogGroupPolicy90D30874" + } + }, + "RestApiECSClustertestlisadevRESTALB34F3E71E": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [ + { + "Key": "deletion_protection.enabled", + "Value": "false" + }, + { + "Key": "idle_timeout.timeout_seconds", + "Value": "600" + }, + { + "Key": "routing.http.drop_invalid_header_fields.enabled", + "Value": "true" + } + ], + "Name": "test-lisa-dev-rest", + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcRestApiAlbSg469AE4F8GroupIdDB418565" + } + ], + "Subnets": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCpublicSubnet1SubnetA91B7DBE8B8D2123" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCpublicSubnet2SubnetC9D5B981D613E068" + } + ], + "Type": "application" + } + }, + "RestApiECSClustertestlisadevRESTALBRESTApplicationListener1A5BC4B4": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "Certificates": [ + { + "CertificateArn": "arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev" + } + ], + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTRESTTgtGrpGroup18249DCC" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "RestApiECSClustertestlisadevRESTALB34F3E71E" + }, + "Port": 443, + "Protocol": "HTTPS", + "SslPolicy": "ELBSecurityPolicy-TLS13-1-2-2021-06" + } + }, + "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTRESTTgtGrpGroup18249DCC": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "HealthCheckIntervalSeconds": 60, + "HealthCheckPath": "/health", + "HealthCheckTimeoutSeconds": 30, + "HealthyThresholdCount": 2, + "Name": "test-lisa-rest-rest", + "Port": 80, + "Protocol": "HTTP", + "TargetGroupAttributes": [ + { + "Key": "stickiness.enabled", + "Value": "false" + } + ], + "TargetType": "instance", + "UnhealthyThresholdCount": 10, + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTMCPWORKBENCHTgtGrpGroupD99D2C75": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "HealthCheckIntervalSeconds": 60, + "HealthCheckPath": "/health", + "HealthCheckTimeoutSeconds": 30, + "HealthyThresholdCount": 2, + "Name": "test-lisa-rest-mcpworkbench", + "Port": 80, + "Protocol": "HTTP", + "TargetGroupAttributes": [ + { + "Key": "stickiness.enabled", + "Value": "false" + } + ], + "TargetType": "instance", + "UnhealthyThresholdCount": 10, + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTMCPWORKBENCHTgtGrpRuleBDFA4026": { + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + "Properties": { + "Actions": [ + { + "TargetGroupArn": { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTMCPWORKBENCHTgtGrpGroupD99D2C75" + }, + "Type": "forward" + } + ], + "Conditions": [ + { + "Field": "path-pattern", + "PathPatternConfig": { + "Values": [ + "/v2/mcp/*" + ] + } + } + ], + "ListenerArn": { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListener1A5BC4B4" + }, + "Priority": 80 + } + }, + "RestApiECSClusterRESTTRPolicy68D7DDA5": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2", + "Arn" + ] + } + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TokenTable3625D248", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:us-iso-east-1:012345678901:parameter", + { + "Ref": "LiteLLMDbConnectionInfoStringParameter1408D668" + } + ] + ] + } + }, + { + "Action": "rds-db:connect", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":rds-db:us-iso-east-1:012345678901:dbuser:", + { + "Fn::GetAtt": [ + "LiteLLMScalingDB7C8AE765", + "DbiResourceId" + ] + }, + "/{{resolve:secretsmanager:", + { + "Ref": "LiteLLMScalingDBSecretAttachmentF62968F9" + }, + ":SecretString:username::}}" + ] + ] + } + }, + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "LiteLLMScalingDBSecretAttachmentF62968F9" + } + }, + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:us-iso-east-1:012345678901:parameter", + { + "Ref": "RegisteredModelsStringParameter36CC6A6A" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RestApiECSClusterRESTTRPolicy68D7DDA5", + "Roles": [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolesRESTC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + "RestApiECSClusterRESTERPolicyA12E13F6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2", + "Arn" + ] + } + }, + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:us-iso-east-1:012345678901:repository/cdk-hnb659fds-container-assets-012345678901-us-iso-east-1" + ] + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RestApiECSClusterRESTERPolicyA12E13F6", + "Roles": [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolesRESTEXC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + "RestApiECSClusterRESTEc2TaskDefinition85643A9A": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "LOG_LEVEL", + "Value": "DEBUG" + }, + { + "Name": "AWS_REGION", + "Value": "us-iso-east-1" + }, + { + "Name": "AWS_REGION_NAME", + "Value": "us-iso-east-1" + }, + { + "Name": "THREADS", + "Value": "2" + }, + { + "Name": "USE_AUTH", + "Value": "true" + }, + { + "Name": "AUTHORITY", + "Value": "test" + }, + { + "Name": "CLIENT_ID", + "Value": "test" + }, + { + "Name": "ADMIN_GROUP", + "Value": "" + }, + { + "Name": "USER_GROUP", + "Value": "" + }, + { + "Name": "JWT_GROUPS_PROP", + "Value": "" + }, + { + "Name": "TOKEN_TABLE_NAME", + "Value": { + "Ref": "TokenTable3625D248" + } + }, + { + "Name": "SSL_CERT_DIR", + "Value": "/etc/pki/tls/certs" + }, + { + "Name": "SSL_CERT_FILE", + "Value": "" + }, + { + "Name": "REQUESTS_CA_BUNDLE", + "Value": "" + }, + { + "Name": "AWS_CA_BUNDLE", + "Value": "" + }, + { + "Name": "CURL_CA_BUNDLE", + "Value": "" + }, + { + "Name": "CORS_ORIGINS", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevRESTALB34F3E71E", + "DNSName" + ] + }, + ",*" + ] + ] + } + }, + { + "Name": "LITELLM_KEY", + "Value": "sk-012345" + }, + { + "Name": "OPENAI_API_KEY", + "Value": "sk-012345" + }, + { + "Name": "TIKTOKEN_CACHE_DIR", + "Value": "/app/TIKTOKEN_CACHE" + }, + { + "Name": "MANAGEMENT_KEY_NAME", + "Value": { + "Fn::GetAtt": [ + "ManagementKeySecretName635E199B", + "Value" + ] + } + }, + { + "Name": "LITELLM_DB_INFO_PS_NAME", + "Value": { + "Ref": "LiteLLMDbConnectionInfoStringParameter1408D668" + } + }, + { + "Name": "REGISTERED_MODELS_PS_NAME", + "Value": { + "Ref": "RegisteredModelsStringParameter36CC6A6A" + } + } + ], + "Essential": true, + "HealthCheck": { + "Command": [ + "CMD-SHELL", + "exit 0" + ], + "Interval": 10, + "Retries": 3, + "StartPeriod": 30, + "Timeout": 5 + }, + "Image": { + "Fn::Sub": "012345678901.dkr.ecr.us-iso-east-1.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-012345678901-us-iso-east-1:85f62b16fca9fb8e5a64c4c7592758917def9a7f624111d41d0058a2e62420c3" + }, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2" + }, + "awslogs-stream-prefix": "REST", + "awslogs-region": "us-iso-east-1" + } + }, + "Memory": 3904, + "MemoryReservation": 2048, + "MountPoints": [ + { + "ContainerPath": "/etc/pki", + "ReadOnly": false, + "SourceVolume": "pki" + } + ], + "Name": "test-lisa-REST", + "PortMappings": [ + { + "ContainerPort": 8080, + "HostPort": 0, + "Protocol": "tcp" + } + ], + "Privileged": false + } + ], + "ExecutionRoleArn": { + "Ref": "SsmParameterValuedevtestlisalisarolesRESTEXC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Family": "test-lisa-REST", + "NetworkMode": "bridge", + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Ref": "SsmParameterValuedevtestlisalisarolesRESTC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Volumes": [ + { + "Host": { + "SourcePath": "/etc/pki" + }, + "Name": "pki" + } + ] + } + }, + "RestApiECSClustertestlisaRESTEc2SvcServiceFD91C0F8": { + "Type": "AWS::ECS::Service", + "Properties": { + "CapacityProviderStrategy": [ + { + "CapacityProvider": { + "Ref": "RestApiECSClustertestlisadevAsgCapacityProviderF18999A7" + }, + "Weight": 1 + } + ], + "Cluster": { + "Ref": "RestApiECSClustertestlisadevClC04148B6" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "EnableECSManagedTags": false, + "HealthCheckGracePeriodSeconds": 60, + "LoadBalancers": [ + { + "ContainerName": "test-lisa-REST", + "ContainerPort": 8080, + "TargetGroupArn": { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTRESTTgtGrpGroup18249DCC" + } + } + ], + "SchedulingStrategy": "REPLICA", + "ServiceName": "REST", + "TaskDefinition": { + "Ref": "RestApiECSClusterRESTEc2TaskDefinition85643A9A" + } + }, + "DependsOn": [ + "RestApiECSClusterRESTTRPolicy68D7DDA5", + "RestApiECSClustertestlisadevASGCAE610D7", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperAlarmCA336253", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperPolicy4621F1CA", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerAlarm17D7099D", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerPolicy2BDAE944", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionAllowInvokeLisaServeRestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic7E3AEC6718E71340", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionB9703624", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRoleDefaultPolicy95F8B781", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRole63B8BEE5", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionTopicBAEEB019", + "RestApiECSClustertestlisadevASGInstanceProfile5F0036EC", + "RestApiECSClustertestlisadevASGInstanceRoleDefaultPolicy763E7201", + "RestApiECSClustertestlisadevASGInstanceRole4F18B1DE", + "RestApiECSClustertestlisadevASGLaunchConfigD5B6F73C", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookAC6818CC", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleDefaultPolicyEC0162A6", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleEA0CB372", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E", + "RestApiECSClustertestlisadevRESTALBRESTApplicationListener1A5BC4B4", + "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTMCPWORKBENCHTgtGrpGroupD99D2C75", + "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTMCPWORKBENCHTgtGrpRuleBDFA4026", + "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTRESTTgtGrpGroup18249DCC" + ] + }, + "RestApiECSClustertestlisaRESTEc2SvcTaskCountTarget661FF93C": { + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + "Properties": { + "MaxCapacity": 10, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "service/", + { + "Ref": "RestApiECSClustertestlisadevClC04148B6" + }, + "/", + { + "Fn::GetAtt": [ + "RestApiECSClustertestlisaRESTEc2SvcServiceFD91C0F8", + "Name" + ] + } + ] + ] + }, + "RoleARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::012345678901:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService" + ] + ] + }, + "ScalableDimension": "ecs:service:DesiredCount", + "ServiceNamespace": "ecs" + }, + "DependsOn": [ + "RestApiECSClusterRESTTRPolicy68D7DDA5", + "RestApiECSClustertestlisadevASGCAE610D7", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperAlarmCA336253", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperPolicy4621F1CA", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerAlarm17D7099D", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerPolicy2BDAE944", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionAllowInvokeLisaServeRestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic7E3AEC6718E71340", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionB9703624", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRoleDefaultPolicy95F8B781", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRole63B8BEE5", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionTopicBAEEB019", + "RestApiECSClustertestlisadevASGInstanceProfile5F0036EC", + "RestApiECSClustertestlisadevASGInstanceRoleDefaultPolicy763E7201", + "RestApiECSClustertestlisadevASGInstanceRole4F18B1DE", + "RestApiECSClustertestlisadevASGLaunchConfigD5B6F73C", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookAC6818CC", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleDefaultPolicyEC0162A6", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleEA0CB372", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E" + ] + }, + "RestApiECSClustertestlisaRESTEc2SvcTaskCountTargetRESTRESTScalingPolicyDD051843": { + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "Properties": { + "PolicyName": "LisaServeRestApiECSClustertestlisaRESTEc2SvcTaskCountTargetRESTRESTScalingPolicy4B95BCA8", + "PolicyType": "TargetTrackingScaling", + "ScalingTargetId": { + "Ref": "RestApiECSClustertestlisaRESTEc2SvcTaskCountTarget661FF93C" + }, + "TargetTrackingScalingPolicyConfiguration": { + "PredefinedMetricSpecification": { + "PredefinedMetricType": "ALBRequestCountPerTarget", + "ResourceLabel": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListener1A5BC4B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListener1A5BC4B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListener1A5BC4B4" + } + ] + } + ] + }, + "/", + { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTRESTTgtGrpGroup18249DCC", + "TargetGroupFullName" + ] + } + ] + ] + } + }, + "ScaleInCooldown": 60, + "ScaleOutCooldown": 60, + "TargetValue": 1000 + } + }, + "DependsOn": [ + "RestApiECSClusterRESTTRPolicy68D7DDA5", + "RestApiECSClustertestlisadevASGCAE610D7", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperAlarmCA336253", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperPolicy4621F1CA", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerAlarm17D7099D", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerPolicy2BDAE944", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionAllowInvokeLisaServeRestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic7E3AEC6718E71340", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionB9703624", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRoleDefaultPolicy95F8B781", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRole63B8BEE5", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionTopicBAEEB019", + "RestApiECSClustertestlisadevASGInstanceProfile5F0036EC", + "RestApiECSClustertestlisadevASGInstanceRoleDefaultPolicy763E7201", + "RestApiECSClustertestlisadevASGInstanceRole4F18B1DE", + "RestApiECSClustertestlisadevASGLaunchConfigD5B6F73C", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookAC6818CC", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleDefaultPolicyEC0162A6", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleEA0CB372", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E" + ] + }, + "RestApiECSClusterMCPWORKBENCHTRPolicy0E033E1A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RestApiECSClusterMCPWORKBENCHTRPolicy0E033E1A", + "Roles": [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolesMCPWORKBENCHC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + "RestApiECSClusterMCPWORKBENCHERPolicy845EAEEA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2", + "Arn" + ] + } + }, + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:us-iso-east-1:012345678901:repository/cdk-hnb659fds-container-assets-012345678901-us-iso-east-1" + ] + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RestApiECSClusterMCPWORKBENCHERPolicy845EAEEA", + "Roles": [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolesMCPWORKBENCHEXC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + "RestApiECSClusterMCPWORKBENCHEc2TaskDefinition7EAF0AD9": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "LOG_LEVEL", + "Value": "DEBUG" + }, + { + "Name": "AWS_REGION", + "Value": "us-iso-east-1" + }, + { + "Name": "AWS_REGION_NAME", + "Value": "us-iso-east-1" + }, + { + "Name": "THREADS", + "Value": "2" + }, + { + "Name": "USE_AUTH", + "Value": "true" + }, + { + "Name": "AUTHORITY", + "Value": "test" + }, + { + "Name": "CLIENT_ID", + "Value": "test" + }, + { + "Name": "ADMIN_GROUP", + "Value": "" + }, + { + "Name": "USER_GROUP", + "Value": "" + }, + { + "Name": "JWT_GROUPS_PROP", + "Value": "" + }, + { + "Name": "TOKEN_TABLE_NAME", + "Value": { + "Ref": "TokenTable3625D248" + } + }, + { + "Name": "SSL_CERT_DIR", + "Value": "/etc/pki/tls/certs" + }, + { + "Name": "SSL_CERT_FILE", + "Value": "" + }, + { + "Name": "REQUESTS_CA_BUNDLE", + "Value": "" + }, + { + "Name": "AWS_CA_BUNDLE", + "Value": "" + }, + { + "Name": "CURL_CA_BUNDLE", + "Value": "" + }, + { + "Name": "CORS_ORIGINS", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevRESTALB34F3E71E", + "DNSName" + ] + }, + ",*" + ] + ] + } + }, + { + "Name": "RCLONE_CONFIG_S3_REGION", + "Value": "us-iso-east-1" + }, + { + "Name": "MCPWORKBENCH_BUCKET", + "Value": "test-lisa-dev-mcpworkbench-012345678901" + } + ], + "Essential": true, + "HealthCheck": { + "Command": [ + "CMD-SHELL", + "exit 0" + ], + "Interval": 10, + "Retries": 3, + "StartPeriod": 30, + "Timeout": 5 + }, + "Image": { + "Fn::Sub": "012345678901.dkr.ecr.us-iso-east-1.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-012345678901-us-iso-east-1:77f2272ae5818f313fdcf22f97d72a5cda130e5acd8b28a79e2b0b9e9844ff5e" + }, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "RestApiECSClusterdevtestlisalisaRESTLogGroup92E80EC2" + }, + "awslogs-stream-prefix": "MCPWORKBENCH", + "awslogs-region": "us-iso-east-1" + } + }, + "Memory": 3904, + "MemoryReservation": 1024, + "MountPoints": [ + { + "ContainerPath": "/etc/pki", + "ReadOnly": false, + "SourceVolume": "pki" + } + ], + "Name": "test-lisa-MCPWORKBENCH", + "PortMappings": [ + { + "ContainerPort": 8000, + "HostPort": 0, + "Protocol": "tcp" + } + ], + "Privileged": true + } + ], + "ExecutionRoleArn": { + "Ref": "SsmParameterValuedevtestlisalisarolesMCPWORKBENCHEXC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Family": "test-lisa-MCPWORKBENCH", + "NetworkMode": "bridge", + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Ref": "SsmParameterValuedevtestlisalisarolesMCPWORKBENCHC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "Volumes": [ + { + "Host": { + "SourcePath": "/etc/pki" + }, + "Name": "pki" + } + ] + } + }, + "RestApiECSClustertestlisaMCPWORKBENCHEc2SvcService1642D1D0": { + "Type": "AWS::ECS::Service", + "Properties": { + "CapacityProviderStrategy": [ + { + "CapacityProvider": { + "Ref": "RestApiECSClustertestlisadevAsgCapacityProviderF18999A7" + }, + "Weight": 1 + } + ], + "Cluster": { + "Ref": "RestApiECSClustertestlisadevClC04148B6" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "EnableECSManagedTags": false, + "HealthCheckGracePeriodSeconds": 60, + "LoadBalancers": [ + { + "ContainerName": "test-lisa-MCPWORKBENCH", + "ContainerPort": 8000, + "TargetGroupArn": { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTMCPWORKBENCHTgtGrpGroupD99D2C75" + } + } + ], + "SchedulingStrategy": "REPLICA", + "ServiceName": "MCPWORKBENCH", + "TaskDefinition": { + "Ref": "RestApiECSClusterMCPWORKBENCHEc2TaskDefinition7EAF0AD9" + } + }, + "DependsOn": [ + "RestApiECSClusterMCPWORKBENCHTRPolicy0E033E1A", + "RestApiECSClustertestlisadevASGCAE610D7", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperAlarmCA336253", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperPolicy4621F1CA", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerAlarm17D7099D", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerPolicy2BDAE944", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionAllowInvokeLisaServeRestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic7E3AEC6718E71340", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionB9703624", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRoleDefaultPolicy95F8B781", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRole63B8BEE5", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionTopicBAEEB019", + "RestApiECSClustertestlisadevASGInstanceProfile5F0036EC", + "RestApiECSClustertestlisadevASGInstanceRoleDefaultPolicy763E7201", + "RestApiECSClustertestlisadevASGInstanceRole4F18B1DE", + "RestApiECSClustertestlisadevASGLaunchConfigD5B6F73C", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookAC6818CC", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleDefaultPolicyEC0162A6", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleEA0CB372", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E", + "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTMCPWORKBENCHTgtGrpRuleBDFA4026" + ] + }, + "RestApiECSClustertestlisaMCPWORKBENCHEc2SvcTaskCountTarget7F3A2827": { + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + "Properties": { + "MaxCapacity": 10, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "service/", + { + "Ref": "RestApiECSClustertestlisadevClC04148B6" + }, + "/", + { + "Fn::GetAtt": [ + "RestApiECSClustertestlisaMCPWORKBENCHEc2SvcService1642D1D0", + "Name" + ] + } + ] + ] + }, + "RoleARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::012345678901:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService" + ] + ] + }, + "ScalableDimension": "ecs:service:DesiredCount", + "ServiceNamespace": "ecs" + }, + "DependsOn": [ + "RestApiECSClusterMCPWORKBENCHTRPolicy0E033E1A", + "RestApiECSClustertestlisadevASGCAE610D7", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperAlarmCA336253", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperPolicy4621F1CA", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerAlarm17D7099D", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerPolicy2BDAE944", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionAllowInvokeLisaServeRestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic7E3AEC6718E71340", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionB9703624", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRoleDefaultPolicy95F8B781", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRole63B8BEE5", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionTopicBAEEB019", + "RestApiECSClustertestlisadevASGInstanceProfile5F0036EC", + "RestApiECSClustertestlisadevASGInstanceRoleDefaultPolicy763E7201", + "RestApiECSClustertestlisadevASGInstanceRole4F18B1DE", + "RestApiECSClustertestlisadevASGLaunchConfigD5B6F73C", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookAC6818CC", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleDefaultPolicyEC0162A6", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleEA0CB372", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E" + ] + }, + "RestApiECSClustertestlisaMCPWORKBENCHEc2SvcTaskCountTargetRESTMCPWORKBENCHScalingPolicy1E2C3D30": { + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "Properties": { + "PolicyName": "LisaServeRestApiECSClustertestlisaMCPWORKBENCHEc2SvcTaskCountTargetRESTMCPWORKBENCHScalingPolicy1B8CBAC5", + "PolicyType": "TargetTrackingScaling", + "ScalingTargetId": { + "Ref": "RestApiECSClustertestlisaMCPWORKBENCHEc2SvcTaskCountTarget7F3A2827" + }, + "TargetTrackingScalingPolicyConfiguration": { + "PredefinedMetricSpecification": { + "PredefinedMetricType": "ALBRequestCountPerTarget", + "ResourceLabel": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListener1A5BC4B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListener1A5BC4B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Ref": "RestApiECSClustertestlisadevRESTALBRESTApplicationListener1A5BC4B4" + } + ] + } + ] + }, + "/", + { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevRESTALBRESTApplicationListenerRESTMCPWORKBENCHTgtGrpGroupD99D2C75", + "TargetGroupFullName" + ] + } + ] + ] + } + }, + "ScaleInCooldown": 60, + "ScaleOutCooldown": 60, + "TargetValue": 1000 + } + }, + "DependsOn": [ + "RestApiECSClusterMCPWORKBENCHTRPolicy0E033E1A", + "RestApiECSClustertestlisadevASGCAE610D7", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperAlarmCA336253", + "RestApiECSClustertestlisadevASGASGRESTScaleInUpperPolicy4621F1CA", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerAlarm17D7099D", + "RestApiECSClustertestlisadevASGASGRESTScaleOutLowerPolicy2BDAE944", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionAllowInvokeLisaServeRestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic7E3AEC6718E71340", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionB9703624", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRoleDefaultPolicy95F8B781", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionServiceRole63B8BEE5", + "RestApiECSClustertestlisadevASGDrainECSHookFunctionTopicBAEEB019", + "RestApiECSClustertestlisadevASGInstanceProfile5F0036EC", + "RestApiECSClustertestlisadevASGInstanceRoleDefaultPolicy763E7201", + "RestApiECSClustertestlisadevASGInstanceRole4F18B1DE", + "RestApiECSClustertestlisadevASGLaunchConfigD5B6F73C", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookAC6818CC", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleDefaultPolicyEC0162A6", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookRoleEA0CB372", + "RestApiECSClustertestlisadevASGLifecycleHookDrainHookTopic4A233E5E" + ] + }, + "FastApiEndpointF31771C0": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/serve/endpoint", + "Type": "String", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevRESTALB34F3E71E", + "DNSName" + ] + } + ] + ] + } + } + }, + "LisaServemanagementEventBusBC0589F0": { + "Type": "AWS::Events::EventBus", + "Properties": { + "Name": "test-lisa-lisa-management-events" + } + }, + "LisaServemanagementKeySecretCFE1B698": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "Description": "LISA management key secret", + "GenerateSecretString": { + "ExcludePunctuation": true, + "PasswordLength": 16 + }, + "Name": "test-lisa-lisa-management-key" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LisaServemanagementKeySecretRotationSchedule2C6A4AE9": { + "Type": "AWS::SecretsManager::RotationSchedule", + "Properties": { + "RotationLambdaARN": { + "Fn::GetAtt": [ + "LisaServemanagementKeyRotationLambda7ED5E511", + "Arn" + ] + }, + "RotationRules": { + "ScheduleExpression": "rate(30 days)" + }, + "SecretId": { + "Ref": "LisaServemanagementKeySecretCFE1B698" + } + }, + "DependsOn": [ + "LisaServemanagementKeyRotationLambdaInvokeN0a2GKfZP0JmDqDEVhhu6A0TUv3NyNbk4YMFKNc296C2F95" + ] + }, + "LisaServemanagementKeySecretPolicy4758F96E": { + "Type": "AWS::SecretsManager::ResourcePolicy", + "Properties": { + "ResourcePolicy": { + "Statement": [ + { + "Action": "secretsmanager:DeleteSecret", + "Effect": "Deny", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::012345678901:root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "SecretId": { + "Ref": "LisaServemanagementKeySecretCFE1B698" + } + } + }, + "LisaServemanagementKeyRotationRoleF44AB827": { + "Type": "AWS::IAM::Role", + "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/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ], + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetSecretValue", + "secretsmanager:PutSecretValue", + "secretsmanager:UpdateSecretVersionStage" + ], + "Effect": "Allow", + "Resource": { + "Ref": "LisaServemanagementKeySecretCFE1B698" + } + }, + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LisaServemanagementEventBusBC0589F0", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "SecretsManagerRotation" + } + ] + } + }, + "LisaServemanagementKeyRotationRoleDefaultPolicy21279630": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetSecretValue", + "secretsmanager:PutSecretValue", + "secretsmanager:UpdateSecretVersionStage" + ], + "Effect": "Allow", + "Resource": { + "Ref": "LisaServemanagementKeySecretCFE1B698" + } + }, + { + "Action": "secretsmanager:GetRandomPassword", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LisaServemanagementKeyRotationRoleDefaultPolicy21279630", + "Roles": [ + { + "Ref": "LisaServemanagementKeyRotationRoleF44AB827" + } + ] + } + }, + "LisaServemanagementKeyRotationLambda7ED5E511": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "d30f155d17212cea93e902d5495d00068001aaa2488e43708bd159abd0871b7a.zip" + }, + "Environment": { + "Variables": { + "EVENT_BUS_NAME": { + "Ref": "LisaServemanagementEventBusBC0589F0" + } + } + }, + "Handler": "management_key.handler", + "Role": { + "Fn::GetAtt": [ + "LisaServemanagementKeyRotationRoleF44AB827", + "Arn" + ] + }, + "Runtime": "python3.11", + "Timeout": 300, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "DependsOn": [ + "LisaServemanagementKeyRotationRoleDefaultPolicy21279630", + "LisaServemanagementKeyRotationRoleF44AB827" + ] + }, + "LisaServemanagementKeyRotationLambdaInvokeN0a2GKfZP0JmDqDEVhhu6A0TUv3NyNbk4YMFKNc296C2F95": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LisaServemanagementKeyRotationLambda7ED5E511", + "Arn" + ] + }, + "Principal": "secretsmanager.amazonaws.com" + } + }, + "ManagementKeySecretName635E199B": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/managementKeySecretName", + "Type": "String", + "Value": { + "Fn::Join": [ + "-", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "-", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "LisaServemanagementKeySecretCFE1B698" + } + ] + } + ] + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "-", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "LisaServemanagementKeySecretCFE1B698" + } + ] + } + ] + } + ] + } + ] + }, + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "-", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "LisaServemanagementKeySecretCFE1B698" + } + ] + } + ] + } + ] + } + ] + }, + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "-", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "LisaServemanagementKeySecretCFE1B698" + } + ] + } + ] + } + ] + } + ] + }, + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + "-", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "LisaServemanagementKeySecretCFE1B698" + } + ] + } + ] + } + ] + } + ] + } + ] + ] + } + } + }, + "LISALiteLLMScalingSg3CC6544C": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for LiteLLM dynamic model management database", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "10.0.0.128/26", + "Description": "Allow REST API private subnets to communicate with LISA-LiteLLMScalingSg", + "FromPort": 5432, + "IpProtocol": "tcp", + "ToPort": 5432 + }, + { + "CidrIp": "10.0.0.192/26", + "Description": "Allow REST API private subnets to communicate with LISA-LiteLLMScalingSg", + "FromPort": 5432, + "IpProtocol": "tcp", + "ToPort": 5432 + }, + { + "CidrIp": "10.0.1.0/26", + "Description": "Allow REST API private subnets to communicate with LISA-LiteLLMScalingSg", + "FromPort": 5432, + "IpProtocol": "tcp", + "ToPort": 5432 + }, + { + "CidrIp": "10.0.1.64/26", + "Description": "Allow REST API private subnets to communicate with LISA-LiteLLMScalingSg", + "FromPort": 5432, + "IpProtocol": "tcp", + "ToPort": 5432 + } + ], + "VpcId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPC8B8C4E4BB8544CDA" + } + } + }, + "LISALiteLLMScalingSgfromLisaNetworkingVpcLambdaSecurityGroupE929D4AD54321CB0EB7E": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "Description": "Allow rotation Lambda to connect to database", + "FromPort": 5432, + "GroupId": { + "Fn::GetAtt": [ + "LISALiteLLMScalingSg3CC6544C", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + }, + "ToPort": 5432 + } + }, + "LiteLLMScalingDBSubnetGroup03777761": { + "Type": "AWS::RDS::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnet group for LiteLLMScalingDB database", + "SubnetIds": [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + } + }, + "LisaServeLiteLLMScalingDBSecret7BF5A8EA3fdaad7efa858a3daf9490cf0a702aeb": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "Description": { + "Fn::Join": [ + "", + [ + "Generated by the CDK for stack: ", + { + "Ref": "AWS::StackName" + } + ] + ] + }, + "GenerateSecretString": { + "ExcludeCharacters": " %+~`#$&*()|[]{}:;<>?!'/@\"\\", + "GenerateStringKey": "password", + "PasswordLength": 30, + "SecretStringTemplate": "{\"username\":\"postgres\"}" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LiteLLMScalingDBSecretAttachmentF62968F9": { + "Type": "AWS::SecretsManager::SecretTargetAttachment", + "Properties": { + "SecretId": { + "Ref": "LisaServeLiteLLMScalingDBSecret7BF5A8EA3fdaad7efa858a3daf9490cf0a702aeb" + }, + "TargetId": { + "Ref": "LiteLLMScalingDB7C8AE765" + }, + "TargetType": "AWS::RDS::DBInstance" + } + }, + "LiteLLMScalingDBSecretAttachmentDatabasePasswordRotationScheduleC8556257": { + "Type": "AWS::SecretsManager::RotationSchedule", + "Properties": { + "HostedRotationLambda": { + "ExcludeCharacters": " %+~`#$&*()|[]{}:;<>?!'/@\"\\", + "RotationLambdaName": "test-lisa-Litellm-Rotation-Function", + "RotationType": "PostgreSQLSingleUser", + "VpcSecurityGroupIds": { + "Fn::ImportValue": "LisaNetworking:ExportsOutputFnGetAttVpcLambdaSecurityGroup184B54BDGroupIdB1374FFB" + }, + "VpcSubnetIds": { + "Fn::Join": [ + "", + [ + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet1Subnet29B9FADC0739E75F" + }, + ",", + { + "Fn::ImportValue": "LisaNetworking:ExportsOutputRefVpcVPCprivateSubnet2Subnet63498DC142E639BD" + } + ] + ] + } + }, + "RotationRules": { + "ScheduleExpression": "rate(30 days)" + }, + "SecretId": { + "Ref": "LiteLLMScalingDBSecretAttachmentF62968F9" + } + } + }, + "LiteLLMScalingDBSecretAttachmentPolicyDE12773F": { + "Type": "AWS::SecretsManager::ResourcePolicy", + "Properties": { + "ResourcePolicy": { + "Statement": [ + { + "Action": "secretsmanager:DeleteSecret", + "Effect": "Deny", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::012345678901:root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Ref": "SsmParameterValuedevtestlisalisarolesRESTC96584B6F00A464EAD1953AFF4B05118Parameter" + } + }, + "Resource": { + "Ref": "LiteLLMScalingDBSecretAttachmentF62968F9" + } + } + ], + "Version": "2012-10-17" + }, + "SecretId": { + "Ref": "LiteLLMScalingDBSecretAttachmentF62968F9" + } + } + }, + "LiteLLMScalingDB7C8AE765": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "AllocatedStorage": "100", + "CopyTagsToSnapshot": true, + "DBInstanceClass": "db.m5.large", + "DBSubnetGroupName": { + "Ref": "LiteLLMScalingDBSubnetGroup03777761" + }, + "EnableIAMDatabaseAuthentication": true, + "Engine": "postgres", + "MasterUserPassword": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "LisaServeLiteLLMScalingDBSecret7BF5A8EA3fdaad7efa858a3daf9490cf0a702aeb" + }, + ":SecretString:password::}}" + ] + ] + }, + "MasterUsername": "postgres", + "StorageType": "gp2", + "VPCSecurityGroups": [ + { + "Fn::GetAtt": [ + "LISALiteLLMScalingSg3CC6544C", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LiteLLMDbConnectionInfoStringParameter1408D668": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "/dev/test-lisa/lisa/LiteLLMDbConnectionInfo", + "Type": "String", + "Value": { + "Fn::Join": [ + "", + [ + "{\"username\":\"postgres\",\"dbHost\":\"", + { + "Fn::GetAtt": [ + "LiteLLMScalingDB7C8AE765", + "Endpoint.Address" + ] + }, + "\",\"dbName\":\"postgres\",\"dbPort\":5432,\"passwordSecretId\":\"", + { + "Fn::Join": [ + "-", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "-", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "LisaServeLiteLLMScalingDBSecret7BF5A8EA3fdaad7efa858a3daf9490cf0a702aeb" + } + ] + } + ] + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "-", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "LisaServeLiteLLMScalingDBSecret7BF5A8EA3fdaad7efa858a3daf9490cf0a702aeb" + } + ] + } + ] + } + ] + } + ] + } + ] + ] + }, + "\"}" + ] + ] + } + } + }, + "LisaServeRestApiUriStringParameterF8D56C8B": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "URI for LISA Serve API", + "Name": "/dev/test-lisa/lisa/lisaServeRestApiUri", + "Type": "String", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevRESTALB34F3E71E", + "DNSName" + ] + } + ] + ] + } + } + }, + "RegisteredModelsStringParameter36CC6A6A": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Serialized JSON of registered models data", + "Name": "/dev/test-lisa/lisa/registeredModels", + "Type": "String", + "Value": "[]" + } + }, + "ModelInvokePermsACDC92EC": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "sagemaker:InvokeEndpoint", + "sagemaker:InvokeEndpointWithResponseStream" + ], + "Effect": "Allow", + "Resource": "arn:*:sagemaker:*:*:endpoint/*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelInvokePermsACDC92EC", + "Roles": [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisarolesRESTC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + } + ] + } + } + }, + "Outputs": { + "RestApiRESTUrl2D81DD2A": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "RestApiECSClustertestlisadevRESTALB34F3E71E", + "DNSName" + ] + } + ] + ] + } + }, + "ExportsOutputFnGetAttTokenTable3625D248ArnC9157255": { + "Value": { + "Fn::GetAtt": [ + "TokenTable3625D248", + "Arn" + ] + }, + "Export": { + "Name": "LisaServe:ExportsOutputFnGetAttTokenTable3625D248ArnC9157255" + } + }, + "ExportsOutputRefTokenTable3625D248A47BF86E": { + "Value": { + "Ref": "TokenTable3625D248" + }, + "Export": { + "Name": "LisaServe:ExportsOutputRefTokenTable3625D248A47BF86E" + } + }, + "ExportsOutputRefRestApiECSClustertestlisadevClC04148B6699D280E": { + "Value": { + "Ref": "RestApiECSClustertestlisadevClC04148B6" + }, + "Export": { + "Name": "LisaServe:ExportsOutputRefRestApiECSClustertestlisadevClC04148B6699D280E" + } + }, + "ExportsOutputFnGetAttRestApiECSClustertestlisaMCPWORKBENCHEc2SvcService1642D1D0Name81C9F72A": { + "Value": { + "Fn::GetAtt": [ + "RestApiECSClustertestlisaMCPWORKBENCHEc2SvcService1642D1D0", + "Name" + ] + }, + "Export": { + "Name": "LisaServe:ExportsOutputFnGetAttRestApiECSClustertestlisaMCPWORKBENCHEc2SvcService1642D1D0Name81C9F72A" + } + } + }, + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" + }, + "SsmParameterValuedevtestlisalisarolesRESTC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/roles/REST" + }, + "SsmParameterValuedevtestlisalisarolesRESTEXC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/roles/RESTEX" + }, + "SsmParameterValuedevtestlisalisarolesMCPWORKBENCHC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/roles/MCPWORKBENCH" + }, + "SsmParameterValuedevtestlisalisarolesMCPWORKBENCHEXC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/roles/MCPWORKBENCHEX" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "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/test/cdk/stacks/__baselines__/LisaUI.json b/test/cdk/stacks/__baselines__/LisaUI.json new file mode 100644 index 000000000..4f8be549f --- /dev/null +++ b/test/cdk/stacks/__baselines__/LisaUI.json @@ -0,0 +1,748 @@ +{ + "Parameters": { + "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/bucket/bucket-access-logs" + }, + "LisaRestApiUriStringParameterParameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/dev/test-lisa/lisa/lisaServeRestApiUri" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Resources": { + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "SsmParameterValuedevtestlisalisabucketbucketaccesslogsC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + ] + } + ] + } + ] + }, + "LogFilePrefix": "logs/website-bucket/" + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + }, + { + "Key": "aws-cdk:cr-owned:489a626c", + "Value": "true" + } + ], + "WebsiteConfiguration": { + "ErrorDocument": "index.html", + "IndexDocument": "index.html" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "BucketPolicyE9A3008A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Bucket83908E77" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:PutBucketPolicy", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "BucketAutoDeleteObjectsCustomResourceBAFD23C2": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "Bucket83908E77" + } + }, + "DependsOn": [ + "BucketPolicyE9A3008A" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": "nodejs22.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "Bucket83908E77" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "LisaUIs3readerrole47B12A4F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": "Allows API gateway to proxy static website assets", + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonS3ReadOnlyAccess" + ] + ] + } + ], + "RoleName": "LisaUI-s3-reader-role" + } + }, + "RestApiGET0F59260B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "NONE", + "HttpMethod": "GET", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "LisaUIs3readerrole47B12A4F", + "Arn" + ] + }, + "IntegrationHttpMethod": "GET", + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Length": "integration.response.header.Content-Length", + "method.response.header.Content-Type": "integration.response.header.Content-Type", + "method.response.header.Content-Disposition": "integration.response.header.Content-Disposition" + }, + "StatusCode": "200" + } + ], + "RequestParameters": { + "integration.request.header.Accept": "method.request.header.Accept", + "integration.request.header.Content-Disposition": "method.request.header.Content-Disposition", + "integration.request.header.Content-Type": "method.request.header.Content-Type" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:s3:path/", + { + "Ref": "Bucket83908E77" + }, + "/index.html" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Length": true, + "method.response.header.Content-Type": true, + "method.response.header.Content-Disposition": true + }, + "StatusCode": "200" + } + ], + "RequestParameters": { + "method.request.header.Accept": true, + "method.request.header.Content-Type": true, + "method.request.header.Content-Disposition": true + }, + "ResourceId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RestApioauth1E8C4FF1": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "oauth", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RestApioauthcallbackD9766F4F": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "RestApioauth1E8C4FF1" + }, + "PathPart": "callback", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RestApioauthcallbackGET4DFECA02": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "NONE", + "HttpMethod": "GET", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "LisaUIs3readerrole47B12A4F", + "Arn" + ] + }, + "IntegrationHttpMethod": "GET", + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Length": "integration.response.header.Content-Length", + "method.response.header.Content-Type": "integration.response.header.Content-Type", + "method.response.header.Content-Disposition": "integration.response.header.Content-Disposition" + }, + "StatusCode": "200" + } + ], + "RequestParameters": { + "integration.request.header.Accept": "method.request.header.Accept", + "integration.request.header.Content-Disposition": "method.request.header.Content-Disposition", + "integration.request.header.Content-Type": "method.request.header.Content-Type" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:s3:path/", + { + "Ref": "Bucket83908E77" + }, + "/index.html" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Length": true, + "method.response.header.Content-Type": true, + "method.response.header.Content-Disposition": true + }, + "StatusCode": "200" + } + ], + "RequestParameters": { + "method.request.header.Accept": true, + "method.request.header.Content-Type": true, + "method.request.header.Content-Disposition": true + }, + "ResourceId": { + "Ref": "RestApioauthcallbackD9766F4F" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RestApiproxyC95856DD": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputFnGetAttLisaApiBaseRestApiBD7EFE41RootResourceId15433D4A" + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "RestApiproxyGET3EA512AF": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "AuthorizationType": "NONE", + "HttpMethod": "GET", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "LisaUIs3readerrole47B12A4F", + "Arn" + ] + }, + "IntegrationHttpMethod": "ANY", + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Length": "integration.response.header.Content-Length", + "method.response.header.Content-Type": "integration.response.header.Content-Type", + "method.response.header.Content-Disposition": "integration.response.header.Content-Disposition" + }, + "StatusCode": "200" + } + ], + "RequestParameters": { + "integration.request.header.Accept": "method.request.header.Accept", + "integration.request.header.Content-Disposition": "method.request.header.Content-Disposition", + "integration.request.header.Content-Type": "method.request.header.Content-Type", + "integration.request.path.proxy": "method.request.path.proxy" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:us-iso-east-1:s3:path/", + { + "Ref": "Bucket83908E77" + }, + "/{proxy}" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Length": true, + "method.response.header.Content-Type": true, + "method.response.header.Content-Disposition": true + }, + "StatusCode": "200" + } + ], + "RequestParameters": { + "method.request.header.Accept": true, + "method.request.header.Content-Type": true, + "method.request.header.Content-Disposition": true, + "method.request.path.proxy": true + }, + "ResourceId": { + "Ref": "RestApiproxyC95856DD" + }, + "RestApiId": { + "Fn::ImportValue": "LisaApiBase:ExportsOutputRefLisaApiBaseRestApiBD7EFE4112A65F2C" + } + } + }, + "AwsExportsDepolymentAwsCliLayer7F7D6C1D": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "c49d356cac773d491c5f7ac148995a1181498a8e289429f8612a7f7e3814f535.zip" + }, + "Description": "/opt/awscli/aws" + } + }, + "AwsExportsDepolymentCustomResource1024MiB42CA9E4D": { + "Type": "Custom::CDKBucketDeployment", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C1024MiBB049752D", + "Arn" + ] + }, + "SourceBucketNames": [ + "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "cdk-hnb659fds-assets-012345678901-us-iso-east-1" + ], + "SourceObjectKeys": [ + "e7d1950ef401262faf75c202f139ccf71a4f9a3c62fedee85354530a22a96213.zip", + "f838a49b8c4929f15d16cb578498d48e7e69f4779260781bd269c0c7b7c52560.zip" + ], + "SourceMarkers": [ + {}, + { + "<>": { + "Ref": "LisaRestApiUriStringParameterParameter" + } + } + ], + "SourceMarkersConfig": [ + {}, + {} + ], + "DestinationBucketName": { + "Ref": "Bucket83908E77" + }, + "WaitForDistributionInvalidation": true, + "RetainOnDelete": false, + "Prune": true, + "OutputObjectKeys": true + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C1024MiBServiceRole739949D8": { + "Type": "AWS::IAM::Role", + "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" + ] + ] + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C1024MiBServiceRoleDefaultPolicy0801355D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::cdk-hnb659fds-assets-012345678901-us-iso-east-1" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::cdk-hnb659fds-assets-012345678901-us-iso-east-1/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C1024MiBServiceRoleDefaultPolicy0801355D", + "Roles": [ + { + "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C1024MiBServiceRole739949D8" + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C1024MiBB049752D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-012345678901-us-iso-east-1", + "S3Key": "3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9.zip" + }, + "Environment": { + "Variables": { + "AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "AwsExportsDepolymentAwsCliLayer7F7D6C1D" + } + ], + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C1024MiBServiceRole739949D8", + "Arn" + ] + }, + "Runtime": "python3.13", + "Timeout": 900 + }, + "DependsOn": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C1024MiBServiceRoleDefaultPolicy0801355D", + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C1024MiBServiceRole739949D8" + ] + } + }, + "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/test/cdk/stacks/overrideConfig.test.ts b/test/cdk/stacks/overrideConfig.test.ts new file mode 100644 index 000000000..c4a76fffc --- /dev/null +++ b/test/cdk/stacks/overrideConfig.test.ts @@ -0,0 +1,86 @@ +/** + 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 { Template } from 'aws-cdk-lib/assertions'; +import { Stack } from 'aws-cdk-lib'; +import MockApp from '../mocks/MockApp'; +import ConfigParser from '../mocks/ConfigParser'; + +describe('Override Configuration Tests', () => { + let stacks: Stack[]; + + beforeAll(() => { + const config = ConfigParser.parseConfig(['config-test.yaml', 'assets.yaml']); + stacks = MockApp.create(config).stacks; + }); + + it('should use provided layer assets instead of building', () => { + // Find the core stack which should contain the layers + const coreStack = stacks.find((stack) => stack.stackName.includes('Core')); + expect(coreStack).toBeDefined(); + + if (coreStack) { + const template = Template.fromStack(coreStack); + const resources = template.toJSON().Resources || {}; + + // Check that Lambda layers are created (using provided assets) + const layerVersions = Object.values(resources).filter((resource: any) => + resource.Type === 'AWS::Lambda::LayerVersion' + ); + + // Should have layer versions when assets are provided + expect(layerVersions.length).toBeGreaterThan(0); + } + }); + + it('should use provided container images instead of building', () => { + stacks.forEach((stack) => { + const template = Template.fromStack(stack); + const resources = template.toJSON().Resources || {}; + + // Check for ECS task definitions using provided images + Object.values(resources).forEach((resource: any) => { + if (resource.Type === 'AWS::ECS::TaskDefinition') { + const containerDefs = resource.Properties?.ContainerDefinitions || []; + containerDefs.forEach((container: any) => { + if (container.Image) { + // Handle both string and CloudFormation intrinsic function formats + const imageRef = typeof container.Image === 'string' + ? container.Image + : JSON.stringify(container.Image); + // Should reference ECR repositories, not build artifacts + expect(imageRef).toMatch(/123456789012.*ecr.*test-(api|mcp|batch)/); + } + }); + } + }); + }); + }); + + it('should not create CodeBuild projects when overrides are provided', () => { + stacks.forEach((stack) => { + const template = Template.fromStack(stack); + const resources = template.toJSON().Resources || {}; + + const buildProjects = Object.values(resources).filter((resource: any) => + resource.Type === 'AWS::CodeBuild::Project' + ); + + // Should have minimal or no build projects when overrides are provided + expect(buildProjects.length).toBeLessThanOrEqual(1); + }); + }); +}); diff --git a/test/cdk/stacks/snapshot.test.ts b/test/cdk/stacks/snapshot.test.ts new file mode 100644 index 000000000..5f08abb38 --- /dev/null +++ b/test/cdk/stacks/snapshot.test.ts @@ -0,0 +1,68 @@ +/** + 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 { Template } from 'aws-cdk-lib/assertions'; +import { Stack } from 'aws-cdk-lib'; +import MockApp from '../mocks/MockApp'; +import fs from 'fs'; +import path from 'path'; + +const BASELINE_DIR = path.join(__dirname, '__baselines__'); + +describe('Stack Migration Tests', () => { + const stacks = MockApp.getStacks(); + + stacks?.forEach((stack: Stack) => { + it(`${stack.stackName} has no resource replacements`, () => { + const template = Template.fromStack(stack); + const current = template.toJSON(); + const baselinePath = path.join(BASELINE_DIR, `${stack.stackName}.json`); + + if (!fs.existsSync(baselinePath)) { + console.warn(`No baseline found for ${stack.stackName}, creating one`); + fs.mkdirSync(BASELINE_DIR, { recursive: true }); + fs.writeFileSync(baselinePath, JSON.stringify(current, null, 2)); + return; + } + + const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf-8')); + const replacements = detectResourceReplacements(baseline, current); + + if (replacements.length > 0) { + console.warn(`\nResource changes detected in ${stack.stackName}:`); + replacements.forEach((r) => console.warn(` - ${r}`)); + } + + expect(replacements).toEqual([]); + }); + }); +}); + +function detectResourceReplacements (baseline: any, current: any): string[] { + const replacements: string[] = []; + const baselineResources = baseline.Resources || {}; + const currentResources = current.Resources || {}; + + for (const [logicalId, baselineResource] of Object.entries(baselineResources)) { + const currentResource = currentResources[logicalId]; + + if (currentResource && (baselineResource as any).Type !== (currentResource as any).Type) { + replacements.push(`Resource ${logicalId} type changed from ${(baselineResource as any).Type} to ${(currentResource as any).Type}`); + } + } + + return replacements; +} From d0974982791f5748f02eb512eb758ff0829f69ed Mon Sep 17 00:00:00 2001 From: bedanley Date: Tue, 28 Oct 2025 13:38:00 -0600 Subject: [PATCH 11/21] Update scaling to cluster level (#532) --- lib/api-base/ecsCluster.ts | 66 ++++++++--------------- lib/api-base/fastApiContainer.ts | 2 +- lib/serve/mcpWorkbenchServiceConstruct.ts | 2 +- 3 files changed, 25 insertions(+), 45 deletions(-) diff --git a/lib/api-base/ecsCluster.ts b/lib/api-base/ecsCluster.ts index 41cac8c2b..9366c5741 100644 --- a/lib/api-base/ecsCluster.ts +++ b/lib/api-base/ecsCluster.ts @@ -111,6 +111,7 @@ export class ECSCluster extends Construct { private readonly baseEnvironment: Record; private readonly autoScalingGroup: AutoScalingGroup; private readonly asgCapacityProvider: AsgCapacityProvider; + private readonly identifier: string; /** * Creates a task definition with its associated container and IAM role (base method). @@ -196,6 +197,7 @@ export class ECSCluster extends Construct { constructor (scope: Construct, id: string, props: ECSClusterProps) { super(scope, id); const { config, identifier, vpc, securityGroup, ecsConfig, environment } = props; + this.identifier = identifier; // Create ECS cluster const cluster = new Cluster(this, createCdkId([config.deploymentName, config.deploymentStage, 'Cl']), { @@ -433,7 +435,6 @@ export class ECSCluster extends Construct { public addTask ( taskName: ECSTasks, taskDefinition: TaskDefinition, - identifier: string ): { service: Ec2Service; targetGroup?: ApplicationTargetGroup } { // Retrieve task role and execution role for the task const taskRole = Role.fromRoleArn( @@ -476,10 +477,6 @@ export class ECSCluster extends Construct { }; const service = new Ec2Service(this, createCdkId([this.config.deploymentName, taskName, 'Ec2Svc']), serviceProps); - const scalableTaskCount = service.autoScaleTaskCount({ - minCapacity: 1, - maxCapacity: 10 - }); service.node.addDependency(this.autoScalingGroup); // Store service reference @@ -488,46 +485,29 @@ export class ECSCluster extends Construct { // Allow load balancer to access the service service.connections.allowFrom(this.loadBalancer, Port.allTcp()); - let targetGroup: ApplicationTargetGroup | undefined; - - // Only create target group if the task has application target configuration - if (taskDefinition.applicationTarget) { - const loadBalancerHealthCheckConfig = this.ecsConfig.loadBalancerConfig.healthCheckConfig; + const loadBalancerHealthCheckConfig = this.ecsConfig.loadBalancerConfig.healthCheckConfig; - targetGroup = this.listener.addTargets(createCdkId([identifier, taskName, 'TgtGrp']), { - targetGroupName: createCdkId([this.config.deploymentName, identifier, taskName], 32, 2).toLowerCase(), - healthCheck: { - path: loadBalancerHealthCheckConfig.path, - interval: Duration.seconds(loadBalancerHealthCheckConfig.interval), - timeout: Duration.seconds(loadBalancerHealthCheckConfig.timeout), - healthyThresholdCount: loadBalancerHealthCheckConfig.healthyThresholdCount, - unhealthyThresholdCount: loadBalancerHealthCheckConfig.unhealthyThresholdCount, - }, - port: 80, - targets: [service], - ...(taskDefinition.applicationTarget.priority && { - priority: taskDefinition.applicationTarget.priority, - conditions: taskDefinition.applicationTarget.conditions?.map(({ type, values }) => { - switch (type) { - case 'pathPatterns': - return ListenerCondition.pathPatterns(values); - } - }) + const targetGroup = this.listener.addTargets(createCdkId([this.identifier, taskName, 'TgtGrp']), { + targetGroupName: createCdkId([this.config.deploymentName, this.identifier, taskName], 32, 2).toLowerCase(), + healthCheck: { + path: loadBalancerHealthCheckConfig.path, + interval: Duration.seconds(loadBalancerHealthCheckConfig.interval), + timeout: Duration.seconds(loadBalancerHealthCheckConfig.timeout), + healthyThresholdCount: loadBalancerHealthCheckConfig.healthyThresholdCount, + unhealthyThresholdCount: loadBalancerHealthCheckConfig.unhealthyThresholdCount, + }, + port: 80, + targets: [service], + ...(taskDefinition.applicationTarget?.priority && { + priority: taskDefinition.applicationTarget.priority, + conditions: taskDefinition.applicationTarget.conditions?.map(({ type, values }) => { + switch (type) { + case 'pathPatterns': + return ListenerCondition.pathPatterns(values); + } }) - }); - - // Store target group reference - this.targetGroups[taskName] = targetGroup; - - if (targetGroup) { - scalableTaskCount.scaleOnRequestCount(createCdkId([identifier, taskName, 'ScalingPolicy']), { - requestsPerTarget: this.ecsConfig.autoScalingConfig.metricConfig.targetValue, - targetGroup, - scaleInCooldown: Duration.seconds(this.ecsConfig.autoScalingConfig.metricConfig.duration), - scaleOutCooldown: Duration.seconds(this.ecsConfig.autoScalingConfig.metricConfig.duration) - }); - } - } + }) + }); return { service, targetGroup }; } diff --git a/lib/api-base/fastApiContainer.ts b/lib/api-base/fastApiContainer.ts index c599d0371..c795cc666 100644 --- a/lib/api-base/fastApiContainer.ts +++ b/lib/api-base/fastApiContainer.ts @@ -195,7 +195,7 @@ export class FastApiContainer extends Construct { applicationTarget: { port: 8080 } - }, props.apiName); + }); if (tokenTable) { // Grant token table access to REST API task role only diff --git a/lib/serve/mcpWorkbenchServiceConstruct.ts b/lib/serve/mcpWorkbenchServiceConstruct.ts index 0a1adad3d..81e29c104 100644 --- a/lib/serve/mcpWorkbenchServiceConstruct.ts +++ b/lib/serve/mcpWorkbenchServiceConstruct.ts @@ -76,7 +76,7 @@ export default class McpWorkbenchServiceConstruct extends Construct { } }; - const { service } = apiCluster.addTask(ECSTasks.MCPWORKBENCH, mcpWorkbenchTaskDefinition, 'REST'); + const { service } = apiCluster.addTask(ECSTasks.MCPWORKBENCH, mcpWorkbenchTaskDefinition); this.service = service; this.createS3EventHandler(config, service); From 51f73aaa6d0c7ccfe4f42fcaf532fc7a5d433c15 Mon Sep 17 00:00:00 2001 From: Bear Danley Date: Tue, 28 Oct 2025 21:16:56 +0000 Subject: [PATCH 12/21] Update make BASE_URL --- Makefile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index f3d6afeef..d461e3a8c 100644 --- a/Makefile +++ b/Makefile @@ -16,8 +16,8 @@ DOCKER_CMD ?= $(or $(CDK_DOCKER),docker) # Function to read config with fallback to base config and default value # Usage: VAR := $(call get_config,property,default_value) define get_config -$(shell test -f $(PROJECT_DIR)/config-custom.yaml && yq $(1) $(PROJECT_DIR)/config-custom.yaml 2>/dev/null | grep -v '^null$$' || \ - (test -f $(PROJECT_DIR)/config-base.yaml && yq $(1) $(PROJECT_DIR)/config-base.yaml 2>/dev/null | grep -v '^null$$') || \ +$(shell test -f $(PROJECT_DIR)/config-custom.yaml && yq -r $(1) $(PROJECT_DIR)/config-custom.yaml 2>/dev/null | grep -v '^null$$' || \ + (test -f $(PROJECT_DIR)/config-base.yaml && yq -r $(1) $(PROJECT_DIR)/config-base.yaml 2>/dev/null | grep -v '^null$$') || \ echo "$(2)") endef @@ -99,13 +99,12 @@ MODEL_BUCKET := $(call get_config,.s3BucketModels,) # BASE_URL - Base URL for web UI assets based on domain name and deployment stage DOMAIN_NAME := $(call get_config,.apiGatewayConfig.domainName,) -ifeq ($(DOMAIN_NAME), null) +ifeq ($(DOMAIN_NAME),) BASE_URL := /$(DEPLOYMENT_STAGE)/ else BASE_URL := / endif - ################################################################################# # COMMANDS # ################################################################################# From 5a71df4691c22996a1cde901ac973d2be89f7232 Mon Sep 17 00:00:00 2001 From: bedanley Date: Tue, 4 Nov 2025 10:10:17 -0700 Subject: [PATCH 13/21] Add function name validation (#533) --- lib/api-base/utils.ts | 7 +- lib/serve/index.ts | 1 + lib/serve/mcpWorkbenchConstruct.ts | 147 ++++++++++++++++++-- lib/serve/mcpWorkbenchServiceConstruct.ts | 160 ---------------------- lib/serve/mcpWorkbenchStack.ts | 21 +-- lib/stages.ts | 2 +- test/cdk/mocks/MockApp.ts | 4 +- 7 files changed, 149 insertions(+), 193 deletions(-) delete mode 100644 lib/serve/mcpWorkbenchServiceConstruct.ts diff --git a/lib/api-base/utils.ts b/lib/api-base/utils.ts index 0d28305b5..3c5f40d59 100644 --- a/lib/api-base/utils.ts +++ b/lib/api-base/utils.ts @@ -85,10 +85,11 @@ export function registerAPIEndpoint ( authorizer?: IAuthorizer, role?: IRole, ): IFunction { - const functionId = `${ + // Validate the function id + const functionId = ( funcDef.id || - [cdk.Stack.of(scope).stackName, funcDef.resource, funcDef.name, funcDef.disambiguator].filter(Boolean).join('-') - }`; + [cdk.Stack.of(scope).stackName, funcDef.resource, funcDef.name, funcDef.disambiguator].filter(Boolean).join('-') + ).replace(/[^a-zA-Z0-9-_]/g, '-').slice(0, 63); const functionResource = getOrCreateResource(scope, api.root, funcDef.path.split('/')); let handler; diff --git a/lib/serve/index.ts b/lib/serve/index.ts index 63d35c797..64b7a21b8 100644 --- a/lib/serve/index.ts +++ b/lib/serve/index.ts @@ -21,6 +21,7 @@ import { FastApiContainer } from '../api-base/fastApiContainer'; import { LisaServeApplicationConstruct, LisaServeApplicationProps } from './serveApplicationConstruct'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; export * from './serveApplicationConstruct'; +export * from './mcpWorkbenchConstruct'; /** * LisaServe Application stack. diff --git a/lib/serve/mcpWorkbenchConstruct.ts b/lib/serve/mcpWorkbenchConstruct.ts index 5459d9f45..3204f7e55 100644 --- a/lib/serve/mcpWorkbenchConstruct.ts +++ b/lib/serve/mcpWorkbenchConstruct.ts @@ -18,33 +18,36 @@ import { IAuthorizer, IRestApi, RestApi } from 'aws-cdk-lib/aws-apigateway'; import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { Construct } from 'constructs'; import { Vpc } from '../networking/vpc'; -import { BaseProps, Config } from '../schema'; +import { BaseProps, Config, EcsSourceType } from '../schema'; import * as s3 from 'aws-cdk-lib/aws-s3'; -import { RemovalPolicy, StackProps } from 'aws-cdk-lib'; +import { Duration, RemovalPolicy, StackProps } from 'aws-cdk-lib'; import { createCdkId } from '../core/utils'; import * as ssm from 'aws-cdk-lib/aws-ssm'; import { getDefaultRuntime, PythonLambdaFunction, registerAPIEndpoint } from '../api-base/utils'; import * as iam from 'aws-cdk-lib/aws-iam'; -import { LAMBDA_PATH } from '../util'; +import { LAMBDA_PATH, MCP_WORKBENCH_PATH } from '../util'; import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import { ECSCluster, ECSTasks } from '../api-base/ecsCluster'; +import { Ec2Service } from 'aws-cdk-lib/aws-ecs'; export type McpWorkbenchConstructProps = { - authorizer: IAuthorizer; restApiId: string; rootResourceId: string; securityGroups: ISecurityGroup[]; vpc: Vpc; + apiCluster: ECSCluster; + authorizer?: IAuthorizer; } & BaseProps & StackProps; -export default class McpWorkbenchConstruct extends Construct { +export class McpWorkbenchConstruct extends Construct { public readonly workbenchBucket: s3.Bucket; constructor (scope: Construct, id: string, props: McpWorkbenchConstructProps) { super(scope, id); - const { authorizer, config, restApiId, rootResourceId, securityGroups, vpc } = props; - - const workbenchBucket = this.createWorkbenchBucket(scope, config); + const { authorizer, config, restApiId, rootResourceId, securityGroups, vpc, apiCluster } = props; // Get common layer based on arn from SSM due to issues with cross stack references const commonLambdaLayer = lambda.LayerVersion.fromLayerVersionArn( @@ -64,14 +67,14 @@ export default class McpWorkbenchConstruct extends Construct { rootResourceId: rootResourceId, }); - const lambdaLayers = [commonLambdaLayer, fastapiLambdaLayer]; - - this.createWorkbenchApi(restApi, rootResourceId, config, vpc, securityGroups, authorizer, workbenchBucket, lambdaLayers); + const lambdaLayers = [commonLambdaLayer, fastapiLambdaLayer]; - this.workbenchBucket = workbenchBucket; + const workbenchBucket = this.createWorkbenchBucket(scope, config); + this.createWorkbenchApi(restApi, config, vpc, securityGroups, workbenchBucket, lambdaLayers, authorizer); + this.createWorkbenchService(apiCluster, config); } - private createWorkbenchApi (restApi: IRestApi, rootResourceId: string, config: Config, vpc: Vpc, securityGroups: ISecurityGroup[], authorizer: IAuthorizer, workbenchBucket: s3.Bucket, lambdaLayers: lambda.ILayerVersion[]) { + private createWorkbenchApi (restApi: IRestApi, config: Config, vpc: Vpc, securityGroups: ISecurityGroup[], workbenchBucket: s3.Bucket, lambdaLayers: lambda.ILayerVersion[], authorizer?: IAuthorizer) { const env = { ADMIN_GROUP: config.authConfig?.adminGroup || '', @@ -188,4 +191,122 @@ export default class McpWorkbenchConstruct extends Construct { eventBridgeEnabled: true }); } + + private createWorkbenchService (apiCluster: ECSCluster, config: Config) { + + const mcpWorkbenchImage = config.mcpWorkbenchConfig || { + baseImage: config.baseImage, + path: MCP_WORKBENCH_PATH, + type: EcsSourceType.ASSET + }; + + const mcpWorkbenchTaskDefinition = { + environment: { + RCLONE_CONFIG_S3_REGION: config.region, + MCPWORKBENCH_BUCKET: [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(), + }, + containerConfig: { + image: mcpWorkbenchImage, + healthCheckConfig: { + command: ['CMD-SHELL', 'exit 0'], + interval: 10, + startPeriod: 30, + timeout: 5, + retries: 3 + }, + environment: {}, + sharedMemorySize: 0, + privileged: true + }, + containerMemoryReservationMiB: 1024, + applicationTarget: { + port: 8000, + priority: 80, + conditions: [{ + type: 'pathPatterns' as const, + values: ['/v2/mcp/*'] + }] + } + }; + + const { service } = apiCluster.addTask(ECSTasks.MCPWORKBENCH, mcpWorkbenchTaskDefinition); + + this.createS3EventHandler(config, service); + } + + private createS3EventHandler (config: any, workbenchService: Ec2Service) { + const s3EventHandlerRole = new iam.Role(this, 'S3EventHandlerRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + inlinePolicies: { + 'S3EventHandlerPolicy': new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents' + ], + resources: [`arn:${config.partition}:logs:*:*:*`] + }), + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'ecs:UpdateService', + 'ecs:DescribeServices', + 'ecs:DescribeClusters' + ], + resources: [ + `arn:${config.partition}:ecs:${config.region}:*:cluster/${workbenchService.cluster.clusterName}*`, + `arn:${config.partition}:ecs:${config.region}:*:service/${workbenchService.cluster.clusterName}*/${workbenchService.serviceName}*` + ] + }), + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'ssm:GetParameter' + ], + resources: [ + `arn:${config.partition}:ssm:${config.region}:*:parameter${config.deploymentPrefix}/deploymentName` + ] + }) + ] + }) + } + }); + + const s3EventHandlerLambda = new lambda.Function(this, 'S3EventHandlerLambda', { + runtime: getDefaultRuntime(), + handler: 'mcp_workbench.s3_event_handler.handler', + code: lambda.Code.fromAsset(config.lambdaPath ?? LAMBDA_PATH), + timeout: Duration.minutes(2), + role: s3EventHandlerRole, + environment: { + DEPLOYMENT_PREFIX: config.deploymentPrefix!, + API_NAME: 'MCPWorkbench', + ECS_CLUSTER_NAME: workbenchService.cluster.clusterName, + MCPWORKBENCH_SERVICE_NAME: workbenchService.serviceName + } + }); + + const rescanMcpWorkbenchRule = new events.Rule(this, 'RescanMCPWorkbenchRule', { + eventPattern: { + source: ['aws.s3', 'debug'], + detailType: [ + 'Object Created', + 'Object Deleted' + ], + detail: { + bucket: { + name: [[config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase()] + } + } + }, + }); + + rescanMcpWorkbenchRule.addTarget(new targets.LambdaFunction(s3EventHandlerLambda, { + retryAttempts: 2, + maxEventAge: Duration.minutes(5) + })); + } } diff --git a/lib/serve/mcpWorkbenchServiceConstruct.ts b/lib/serve/mcpWorkbenchServiceConstruct.ts deleted file mode 100644 index 81e29c104..000000000 --- a/lib/serve/mcpWorkbenchServiceConstruct.ts +++ /dev/null @@ -1,160 +0,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 { Duration } from 'aws-cdk-lib'; -import { Construct } from 'constructs'; -import { BaseProps, EcsSourceType } from '../schema'; -import { Ec2Service } from 'aws-cdk-lib/aws-ecs'; -import { ECSCluster, ECSTasks } from '../api-base/ecsCluster'; -import { MCP_WORKBENCH_PATH } from '../util'; -import * as events from 'aws-cdk-lib/aws-events'; -import * as targets from 'aws-cdk-lib/aws-events-targets'; -import * as lambda from 'aws-cdk-lib/aws-lambda'; -import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; -import { LAMBDA_PATH } from '../util'; -import { getDefaultRuntime } from '../api-base/utils'; -import { IBucket } from 'aws-cdk-lib/aws-s3'; - -export type McpWorkbenchServiceConstructProps = { - apiCluster: ECSCluster; - workbenchBucket: IBucket; -} & BaseProps; - -export default class McpWorkbenchServiceConstruct extends Construct { - public readonly service: Ec2Service; - - constructor (scope: Construct, id: string, props: McpWorkbenchServiceConstructProps) { - super(scope, id); - - const { config, apiCluster } = props; - - const mcpWorkbenchImage = config.mcpWorkbenchConfig || { - baseImage: config.baseImage, - path: MCP_WORKBENCH_PATH, - type: EcsSourceType.ASSET - }; - - const mcpWorkbenchTaskDefinition = { - environment: { - RCLONE_CONFIG_S3_REGION: config.region, - MCPWORKBENCH_BUCKET: [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(), - }, - containerConfig: { - image: mcpWorkbenchImage, - healthCheckConfig: { - command: ['CMD-SHELL', 'exit 0'], - interval: 10, - startPeriod: 30, - timeout: 5, - retries: 3 - }, - environment: {}, - sharedMemorySize: 0, - privileged: true - }, - containerMemoryReservationMiB: 1024, - applicationTarget: { - port: 8000, - priority: 80, - conditions: [{ - type: 'pathPatterns' as const, - values: ['/v2/mcp/*'] - }] - } - }; - - const { service } = apiCluster.addTask(ECSTasks.MCPWORKBENCH, mcpWorkbenchTaskDefinition); - this.service = service; - - this.createS3EventHandler(config, service); - } - - private createS3EventHandler (config: any, workbenchService: Ec2Service) { - const s3EventHandlerRole = new Role(this, 'S3EventHandlerRole', { - assumedBy: new ServicePrincipal('lambda.amazonaws.com'), - inlinePolicies: { - 'S3EventHandlerPolicy': new PolicyDocument({ - statements: [ - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'logs:CreateLogGroup', - 'logs:CreateLogStream', - 'logs:PutLogEvents' - ], - resources: [`arn:${config.partition}:logs:*:*:*`] - }), - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'ecs:UpdateService', - 'ecs:DescribeServices', - 'ecs:DescribeClusters' - ], - resources: [ - `arn:${config.partition}:ecs:${config.region}:*:cluster/${workbenchService.cluster.clusterName}*`, - `arn:${config.partition}:ecs:${config.region}:*:service/${workbenchService.cluster.clusterName}*/${workbenchService.serviceName}*` - ] - }), - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'ssm:GetParameter' - ], - resources: [ - `arn:${config.partition}:ssm:${config.region}:*:parameter${config.deploymentPrefix}/deploymentName` - ] - }) - ] - }) - } - }); - - const s3EventHandlerLambda = new lambda.Function(this, 'S3EventHandlerLambda', { - runtime: getDefaultRuntime(), - handler: 'mcp_workbench.s3_event_handler.handler', - code: lambda.Code.fromAsset(config.lambdaPath ?? LAMBDA_PATH), - timeout: Duration.minutes(2), - role: s3EventHandlerRole, - environment: { - DEPLOYMENT_PREFIX: config.deploymentPrefix!, - API_NAME: 'MCPWorkbench', - ECS_CLUSTER_NAME: workbenchService.cluster.clusterName, - MCPWORKBENCH_SERVICE_NAME: workbenchService.serviceName - } - }); - - const rescanMcpWorkbenchRule = new events.Rule(this, 'RescanMCPWorkbenchRule', { - eventPattern: { - source: ['aws.s3', 'debug'], - detailType: [ - 'Object Created', - 'Object Deleted' - ], - detail: { - bucket: { - name: [[config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase()] - } - } - }, - }); - - rescanMcpWorkbenchRule.addTarget(new targets.LambdaFunction(s3EventHandlerLambda, { - retryAttempts: 2, - maxEventAge: Duration.minutes(5) - })); - } -} diff --git a/lib/serve/mcpWorkbenchStack.ts b/lib/serve/mcpWorkbenchStack.ts index 97ff4fdc1..d95a677ae 100644 --- a/lib/serve/mcpWorkbenchStack.ts +++ b/lib/serve/mcpWorkbenchStack.ts @@ -17,40 +17,33 @@ import { Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { BaseProps } from '../schema'; -import McpWorkbenchConstruct from './mcpWorkbenchConstruct'; +import { McpWorkbenchConstruct } from './mcpWorkbenchConstruct'; import { Vpc } from '../networking/vpc'; import { ECSCluster } from '../api-base/ecsCluster'; -import McpWorkbenchServiceConstruct from './mcpWorkbenchServiceConstruct'; +import { IAuthorizer } from 'aws-cdk-lib/aws-apigateway'; export type McpWorkbenchStackProps = { vpc: Vpc; restApiId: string; rootResourceId: string; - authorizerId: string; apiCluster: ECSCluster; + authorizer?: IAuthorizer; } & BaseProps & StackProps; export class McpWorkbenchStack extends Stack { constructor (scope: Construct, id: string, props: McpWorkbenchStackProps) { super(scope, id, props); - const { config, vpc, restApiId, rootResourceId, authorizerId, apiCluster } = props; + const { vpc, restApiId, rootResourceId, authorizer, apiCluster } = props; - // Import authorizer - const authorizer = { authorizerId }; - - const { workbenchBucket } = new McpWorkbenchConstruct(this, 'McpWorkbench', { + new McpWorkbenchConstruct(this, 'McpWorkbench', { ...props, - authorizer: authorizer as any, restApiId, rootResourceId, securityGroups: [vpc.securityGroups.ecsModelAlbSg], - }); - - new McpWorkbenchServiceConstruct(this, 'McpWorkbenchService', { - config, + vpc: vpc, apiCluster, - workbenchBucket, + authorizer }); } diff --git a/lib/stages.ts b/lib/stages.ts index 2c772d67f..b98fa3fd3 100644 --- a/lib/stages.ts +++ b/lib/stages.ts @@ -302,8 +302,8 @@ export class LisaServeApplicationStage extends Stage { vpc: networkingStack.vpc, restApiId: apiBaseStack.restApiId, rootResourceId: apiBaseStack.rootResourceId, - authorizerId: apiBaseStack.authorizer?.authorizerId || '', apiCluster: serveStack.restApi.apiCluster, + authorizer: apiBaseStack.authorizer, }); mcpWorkbenchStack.addDependency(coreStack); mcpWorkbenchStack.addDependency(apiBaseStack); diff --git a/test/cdk/mocks/MockApp.ts b/test/cdk/mocks/MockApp.ts index 9590e1e63..9e46b7f9e 100644 --- a/test/cdk/mocks/MockApp.ts +++ b/test/cdk/mocks/MockApp.ts @@ -166,8 +166,8 @@ export default class MockApp { vpc: networkingStack.vpc, restApiId: apiBaseStack.restApiId, rootResourceId: apiBaseStack.rootResourceId, - authorizerId: apiBaseStack.authorizer?.authorizerId || '', - apiCluster: serveStack.restApi.apiCluster + apiCluster: serveStack.restApi.apiCluster, + authorizer: apiBaseStack.authorizer }); const stacks: cdk.Stack[] = [ From efa19df781f68d7b5099fab6758a07307cf3d142 Mon Sep 17 00:00:00 2001 From: Dustin Sweigart Date: Tue, 4 Nov 2025 12:14:12 -0500 Subject: [PATCH 14/21] Add offline/air-gapped deployment support for REST API and MCP Workbench (#537) * rebase changes on develop * fixed logic for using pre-cached nodeenv * reverted accidental format changes --------- Co-authored-by: Evan Stohlmann --- lib/api-base/fastApiContainer.ts | 14 +++++++++ lib/docs/admin/deploy.md | 38 ++++++++++++++++++++++- lib/schema/configSchema.ts | 8 +++++ lib/serve/rest-api/Dockerfile | 16 ++++++++++ lib/serve/rest-api/NODEENV_CACHE/.gitkeep | 2 ++ 5 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 lib/serve/rest-api/NODEENV_CACHE/.gitkeep diff --git a/lib/api-base/fastApiContainer.ts b/lib/api-base/fastApiContainer.ts index c795cc666..0ffca9d5a 100644 --- a/lib/api-base/fastApiContainer.ts +++ b/lib/api-base/fastApiContainer.ts @@ -80,6 +80,20 @@ export class FastApiContainer extends Construct { LITELLM_CONFIG: yamlDump(config.litellmConfig), }; + // Add build config overrides if provided + if (config.restApiConfig.buildConfig?.NODEENV_CACHE_DIR) { + buildArgs.NODEENV_CACHE_DIR = config.restApiConfig.buildConfig.NODEENV_CACHE_DIR; + } + + // Add MCP Workbench build config overrides if provided + if (config.mcpWorkbenchBuildConfig) { + Object.entries(config.mcpWorkbenchBuildConfig).forEach(([key, value]) => { + if (value) { + buildArgs[key] = value; + } + }); + } + // Environment variables for all containers const environment: Record = { LOG_LEVEL: config.logLevel, diff --git a/lib/docs/admin/deploy.md b/lib/docs/admin/deploy.md index cdd605d82..888d15fa0 100644 --- a/lib/docs/admin/deploy.md +++ b/lib/docs/admin/deploy.md @@ -283,7 +283,7 @@ Update your `config-custom.yaml` to point to ADC-accessible repositories: ```yaml # Configure pip to use ADC-accessible PyPI mirror -pipConfig: +pypiConfig: indexUrl: https://your-adc-pypi-mirror.com/simple trustedHost: your-adc-pypi-mirror.com @@ -293,9 +293,45 @@ npmConfig: # Use ADC-accessible base images for LISA-Serve and Batch Ingestion baseImage: /python:3.11 + +# Configure offline build dependencies for REST API (nodeenv for prisma-client-py) +restApiConfig: + buildConfig: + NODEENV_CACHE_DIR: "./nodeenv-cache" # Path relative to lib/serve/rest-api/ + +# Configure offline build dependencies for MCP Workbench (S6 Overlay and rclone) +mcpWorkbenchBuildConfig: + S6_OVERLAY_NOARCH_SOURCE: "./s6-overlay-noarch.tar.xz" # Path relative to lib/serve/mcp-workbench/ + S6_OVERLAY_ARCH_SOURCE: "./s6-overlay-x86_64.tar.xz" # Path relative to lib/serve/mcp-workbench/ + RCLONE_SOURCE: "./rclone-linux-amd64.zip" # Path relative to lib/serve/mcp-workbench/ ``` You'll also want any model hosting base containers available, e.g. vllm/vllm-openai:latest and ghcr.io/huggingface/text-embeddings-inference:latest +#### Preparing Offline Build Dependencies + +For environments without internet access during Docker builds, you can pre-cache required dependencies: + +**REST API nodeenv cache** (required by prisma-client-py): +```bash +# Create the cache directory in the REST API build context +python -m nodeenv lib/serve/rest-api/nodeenv-cache +``` + +**MCP Workbench dependencies** (S6 Overlay and rclone): +```bash +# Download S6 Overlay files +cd lib/serve/mcp-workbench/ +wget https://github.com/just-containers/s6-overlay/releases/download/v3.1.6.2/s6-overlay-noarch.tar.xz +wget https://github.com/just-containers/s6-overlay/releases/download/v3.1.6.2/s6-overlay-x86_64.tar.xz + +# Download rclone +wget https://github.com/rclone/rclone/releases/download/v1.71.0/rclone-v1.71.0-linux-amd64.zip + +cd ../../.. +``` + +These cached dependencies will be used during the Docker build process instead of downloading from the internet. + To utilize the prebuilt hosting model containers with self-hosted models, select `type: ecr` in the Model Deployment > Container Configs. ### Deployment Steps diff --git a/lib/schema/configSchema.ts b/lib/schema/configSchema.ts index 573c5f094..7734d74f6 100644 --- a/lib/schema/configSchema.ts +++ b/lib/schema/configSchema.ts @@ -710,6 +710,9 @@ const FastApiContainerConfigSchema = z.object({ domainName: z.string().nullish().default(null), sslCertIamArn: z.string().nullish().default(null).describe('ARN of the self-signed cert to be used throughout the system'), imageConfig: ImageAssetSchema.optional().describe('Override image configuration for ECS FastAPI Containers'), + buildConfig: z.object({ + NODEENV_CACHE_DIR: z.string().optional().describe('Override with a path relative to the build directory for a pre-cached nodeenv directory. Defaults to NODEENV_CACHE. For offline environments, populate using: python -m nodeenv PATH') + }).default({}), rdsConfig: RdsInstanceConfig .default({ dbName: 'postgres', @@ -817,6 +820,11 @@ export const RawConfigObject = z.object({ domain: z.string().default('amazonaws.com').describe('AWS domain for deployment'), restApiConfig: FastApiContainerConfigSchema.describe('Image override for Rest API'), mcpWorkbenchConfig: ImageAssetSchema.optional().describe('Image override for MCP Workbench'), + mcpWorkbenchBuildConfig: z.object({ + S6_OVERLAY_NOARCH_SOURCE: z.string().optional().describe('Override the URL with a path relative to the build directory for the architecture independent S6 overlay tar.xz.'), + S6_OVERLAY_ARCH_SOURCE: z.string().optional().describe('Override the URL with a path relative to the build directory for the architecture specific S6 overlay tar.xz.'), + RCLONE_SOURCE: z.string().optional().describe('Override the URL with a path relative to the build directory for an rclone .zip') + }).default({}), batchIngestionConfig: ImageAssetSchema.optional().describe('Image override for Batch Ingestion'), vpcId: z.string().optional().describe('VPC ID for the application. (e.g. vpc-0123456789abcdef)'), subnets: z.array(z.object({ diff --git a/lib/serve/rest-api/Dockerfile b/lib/serve/rest-api/Dockerfile index 325aefff3..c57ce6354 100644 --- a/lib/serve/rest-api/Dockerfile +++ b/lib/serve/rest-api/Dockerfile @@ -25,6 +25,22 @@ WORKDIR /app COPY src/requirements.txt . RUN pip install --no-cache-dir --upgrade -r requirements.txt +# Copy nodeenv cache directory (always exists, may be empty or populated) +COPY ${NODEENV_CACHE_DIR} /tmp/nodeenv-cache/ + +# Pre-cache nodeenv for prisma-client-py +# If the copied directory has content, use it (for offline environments) +# Otherwise, download it during build (requires internet) +RUN mkdir -p /root/.cache/prisma-python && \ + if [ -d "/tmp/nodeenv-cache" ] && [ -n "$(ls /tmp/nodeenv-cache 2>/dev/null)" ]; then \ + echo "Using pre-cached nodeenv from host" && \ + cp -r /tmp/nodeenv-cache /root/.cache/prisma-python/nodeenv && \ + rm -rf /tmp/nodeenv-cache; \ + else \ + echo "Downloading nodeenv (requires internet)" && \ + python -m nodeenv /root/.cache/prisma-python/nodeenv; \ + fi + # Copy the source code into the container COPY src/ ./src diff --git a/lib/serve/rest-api/NODEENV_CACHE/.gitkeep b/lib/serve/rest-api/NODEENV_CACHE/.gitkeep new file mode 100644 index 000000000..b098f0c35 --- /dev/null +++ b/lib/serve/rest-api/NODEENV_CACHE/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder to ensure NODEENV_CACHE directory exists in build context +# For offline builds, populate this directory using: python -m nodeenv NODEENV_CACHE From 52e32574b7e57b514e485e55b83e94de8ec31669 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Thu, 6 Nov 2025 11:59:53 -0700 Subject: [PATCH 15/21] Feature - bedrock guardrails --- lambda/models/clients/litellm_client.py | 97 ++++++ lambda/models/domain_objects.py | 66 +++- lambda/models/handler/base_handler.py | 2 + lambda/models/handler/get_model_handler.py | 6 +- lambda/models/handler/list_models_handler.py | 11 +- lambda/models/handler/update_model_handler.py | 10 +- lambda/models/handler/utils.py | 56 +++- lambda/models/lambda_functions.py | 6 + lambda/models/state_machine/create_model.py | 143 ++++++++- lambda/models/state_machine/delete_model.py | 63 ++++ lambda/models/state_machine/update_model.py | 272 ++++++++++++++++- lib/models/guardrails-table.ts | 73 +++++ lib/models/model-api.ts | 34 ++- lib/models/state-machine/create-model.ts | 33 +- lib/models/state-machine/delete-model.ts | 24 +- lib/models/state-machine/update-model.ts | 31 +- .../api/endpoints/v2/litellm_passthrough.py | 196 +++++++++++- lib/serve/rest-api/src/auth.py | 44 +++ lib/serve/rest-api/src/utils/guardrails.py | 239 +++++++++++++++ lib/serve/serveApplicationConstruct.ts | 41 ++- .../components/chatbot/hooks/chat.hooks.tsx | 86 ++++-- .../create-model/AutoScalingConfig.tsx | 62 ++-- .../create-model/BaseModelConfig.tsx | 230 +++++++------- .../create-model/ContainerConfig.tsx | 158 +++++----- .../create-model/CreateModelModal.tsx | 21 +- .../create-model/GuardrailsConfig.tsx | 283 ++++++++++++++++++ .../create-model/LoadBalancerConfig.tsx | 24 +- .../react/src/components/types.tsx | 3 + .../react/src/components/utils.ts | 25 +- .../react/src/shared/form/array-input.tsx | 14 +- .../shared/model/model-management.model.ts | 44 +++ .../src/shared/reducers/session.reducer.ts | 2 + .../lambda/test_create_model_state_machine.py | 61 ++++ .../lambda/test_delete_model_state_machine.py | 78 +++++ test/lambda/test_models_lambda.py | 62 +++- .../lambda/test_update_model_state_machine.py | 87 ++++++ 36 files changed, 2385 insertions(+), 302 deletions(-) create mode 100644 lib/models/guardrails-table.ts create mode 100644 lib/serve/rest-api/src/utils/guardrails.py create mode 100644 lib/user-interface/react/src/components/model-management/create-model/GuardrailsConfig.tsx diff --git a/lambda/models/clients/litellm_client.py b/lambda/models/clients/litellm_client.py index 343dd12b9..11fc6546c 100644 --- a/lambda/models/clients/litellm_client.py +++ b/lambda/models/clients/litellm_client.py @@ -98,3 +98,100 @@ def get_model(self, identifier: str) -> Dict[str, Any]: if len(filtered_models) < 1: raise ModelNotFoundError("Specified model was not found.") return filtered_models[0] + + def create_guardrail(self, guardrail_config: Dict[str, Any]) -> Dict[str, Any]: + """ + Create a new guardrail configuration in LiteLLM. + + Args: + guardrail_config: Dictionary containing guardrail configuration including + guardrail_name, guardrail_identifier, guardrail_version, mode, etc. + + Returns: + Dictionary containing the created guardrail information including LiteLLM guardrail ID + """ + resp = requests.post( + self._base_uri + "/guardrails", + headers=self._headers, + json=guardrail_config, + timeout=self._timeout, + verify=self._verify, + ) + resp.raise_for_status() + return resp.json() # type: ignore [no-any-return] + + def update_guardrail(self, guardrail_id: str, guardrail_config: Dict[str, Any]) -> Dict[str, Any]: + """ + Update an existing guardrail configuration in LiteLLM. + + Args: + guardrail_id: The LiteLLM guardrail ID to update + guardrail_config: Dictionary containing updated guardrail configuration + + Returns: + Dictionary containing the updated guardrail information + """ + resp = requests.put( + self._base_uri + f"/guardrails/{guardrail_id}", + headers=self._headers, + json=guardrail_config, + timeout=self._timeout, + verify=self._verify, + ) + resp.raise_for_status() + return resp.json() # type: ignore [no-any-return] + + def delete_guardrail(self, guardrail_id: str) -> None: + """ + Delete a guardrail configuration from LiteLLM. + + Args: + guardrail_id: The LiteLLM guardrail ID to delete + """ + resp = requests.delete( + self._base_uri + f"/guardrails/{guardrail_id}", + headers=self._headers, + timeout=self._timeout, + verify=self._verify, + ) + resp.raise_for_status() + + def get_guardrail_info(self, guardrail_id: str) -> Dict[str, Any]: + """ + Get information about a specific guardrail. + + Args: + guardrail_id: The LiteLLM guardrail ID to retrieve + + Returns: + Dictionary containing guardrail information + """ + resp = requests.get( + self._base_uri + f"/guardrails/{guardrail_id}", + headers=self._headers, + timeout=self._timeout, + verify=self._verify, + ) + resp.raise_for_status() + return resp.json() # type: ignore [no-any-return] + + def apply_guardrail(self, guardrail_name: str, text: str) -> Dict[str, Any]: + """ + Apply a guardrail to text content for validation. + + Args: + guardrail_name: Name of the guardrail to apply + text: Text content to validate against the guardrail + + Returns: + Dictionary containing validation results + """ + resp = requests.post( + self._base_uri + "/guardrails/apply_guardrail", + headers=self._headers, + json={"guardrail_name": guardrail_name, "text": text}, + timeout=self._timeout, + verify=self._verify, + ) + resp.raise_for_status() + return resp.json() # type: ignore [no-any-return] diff --git a/lambda/models/domain_objects.py b/lambda/models/domain_objects.py index 195d11d0f..d2282d114 100644 --- a/lambda/models/domain_objects.py +++ b/lambda/models/domain_objects.py @@ -73,6 +73,65 @@ def __str__(self) -> str: EMBEDDING = "embedding" +class GuardrailMode(str, Enum): + """Defines supported guardrail execution modes.""" + + def __str__(self) -> str: + """Returns string representation of the enum value.""" + return str(self.value) + + PRE_CALL = "pre_call" + DURING_CALL = "during_call" + POST_CALL = "post_call" + + +class GuardrailConfig(BaseModel): + """Defines configuration for a single guardrail.""" + + guardrailName: str = Field(min_length=1) + guardrailIdentifier: str = Field(min_length=1) + guardrailVersion: str = Field(default="DRAFT") + mode: GuardrailMode = Field(default=GuardrailMode.PRE_CALL) + description: Optional[str] = None + allowedGroups: List[str] = Field(default_factory=list) + markedForDeletion: Optional[bool] = Field(default=False) + + +# Type alias for guardrails configuration - maps guardrail IDs to their configs +GuardrailsConfig: TypeAlias = Dict[str, GuardrailConfig] + + +class GuardrailRequest(BaseModel): + """Defines request structure for guardrails API operations.""" + + model_id: str = Field(min_length=1) + guardrails_config: GuardrailsConfig + + +class GuardrailResponse(BaseModel): + """Defines response structure for guardrails API operations.""" + + model_id: str + guardrails_config: GuardrailsConfig + success: bool + message: str + + +class GuardrailsTableEntry(BaseModel): + """Represents a guardrail entry in DynamoDB table.""" + + guardrailId: str # Partition key + modelId: str # Sort key + guardrailName: str + guardrailIdentifier: str + guardrailVersion: str + mode: str + description: Optional[str] + allowedGroups: List[str] + createdDate: int = Field(default_factory=lambda: int(time.time() * 1000)) + lastModifiedDate: int = Field(default_factory=lambda: int(time.time() * 1000)) + + class MetricConfig(BaseModel): """Defines metrics configuration for auto-scaling policies.""" @@ -230,6 +289,7 @@ class LISAModel(BaseModel): streaming: bool features: Optional[List[ModelFeature]] = None allowedGroups: Optional[List[str]] = None + guardrailsConfig: Optional[GuardrailsConfig] = None class ApiResponseBase(BaseModel): @@ -255,6 +315,7 @@ class CreateModelRequest(BaseModel): features: Optional[List[ModelFeature]] = None allowedGroups: Optional[List[str]] = None apiKey: Optional[str] = None + guardrailsConfig: Optional[GuardrailsConfig] = None @model_validator(mode="after") def validate_create_model_request(self) -> Self: @@ -308,6 +369,7 @@ class UpdateModelRequest(BaseModel): allowedGroups: Optional[List[str]] = None features: Optional[List[ModelFeature]] = None containerConfig: Optional[ContainerConfigUpdatable] = None + guardrailsConfig: Optional[GuardrailsConfig] = None @model_validator(mode="after") def validate_update_model_request(self) -> Self: @@ -321,11 +383,13 @@ def validate_update_model_request(self) -> Self: self.allowedGroups, self.features, self.containerConfig, + self.guardrailsConfig, ] if not validate_any_fields_defined(fields): raise ValueError( "At least one field out of autoScalingInstanceConfig, containerConfig, enabled, modelType, " - "modelDescription, streaming, allowedGroups, or features must be defined in request payload." + "modelDescription, streaming, allowedGroups, features, or guardrailsConfig must be " + "defined in request payload." ) if self.modelType == ModelType.EMBEDDING and self.streaming: diff --git a/lambda/models/handler/base_handler.py b/lambda/models/handler/base_handler.py index e165ab913..66ac1c30e 100644 --- a/lambda/models/handler/base_handler.py +++ b/lambda/models/handler/base_handler.py @@ -26,11 +26,13 @@ def __init__( autoscaling_client: Any, stepfunctions_client: Any, model_table_resource: Any, + guardrails_table_resource: Any, ): """Make all clients available for use in any handler class.""" self._autoscaling = autoscaling_client self._stepfunctions = stepfunctions_client self._model_table = model_table_resource + self._guardrails_table = guardrails_table_resource def __call__(self, *args: Any, **kwargs: Any) -> Any: """All handlers must implement the __call__ method.""" diff --git a/lambda/models/handler/get_model_handler.py b/lambda/models/handler/get_model_handler.py index 675381953..c6b82d520 100644 --- a/lambda/models/handler/get_model_handler.py +++ b/lambda/models/handler/get_model_handler.py @@ -21,7 +21,7 @@ from ..domain_objects import GetModelResponse from ..exception import ModelNotFoundError from .base_handler import BaseApiHandler -from .utils import to_lisa_model +from .utils import attach_guardrails_to_model, fetch_guardrails_for_model, to_lisa_model class GetModelHandler(BaseApiHandler): @@ -37,6 +37,10 @@ def __call__( model = to_lisa_model(ddb_item) + # Fetch and attach guardrails for this model + guardrail_items = fetch_guardrails_for_model(self._guardrails_table, model_id) + attach_guardrails_to_model(model, guardrail_items) + # Check if user has access to this model based on groups if not is_admin and user_groups is not None: if not user_has_group_access(user_groups, model.allowedGroups or []): diff --git a/lambda/models/handler/list_models_handler.py b/lambda/models/handler/list_models_handler.py index 56b29fd7b..c1f27ef90 100644 --- a/lambda/models/handler/list_models_handler.py +++ b/lambda/models/handler/list_models_handler.py @@ -20,7 +20,7 @@ from ..domain_objects import ListModelsResponse from .base_handler import BaseApiHandler -from .utils import to_lisa_model +from .utils import attach_guardrails_to_model, fetch_all_guardrails, group_guardrails_by_model, to_lisa_model class ListModelsHandler(BaseApiHandler): @@ -39,6 +39,15 @@ def __call__(self, user_groups: Optional[List[str]] = None, is_admin: bool = Fal models_list = [to_lisa_model(m) for m in ddb_models] + # Fetch all guardrails and group them by model ID + all_guardrails = fetch_all_guardrails(self._guardrails_table) + guardrails_by_model = group_guardrails_by_model(all_guardrails) + + # Attach guardrails to models + for model in models_list: + if model.modelId in guardrails_by_model: + attach_guardrails_to_model(model, guardrails_by_model[model.modelId]) + # Filter models based on user groups if not admin if not is_admin and user_groups is not None: models_list = [ diff --git a/lambda/models/handler/update_model_handler.py b/lambda/models/handler/update_model_handler.py index 6ae691a28..4d21d631f 100644 --- a/lambda/models/handler/update_model_handler.py +++ b/lambda/models/handler/update_model_handler.py @@ -20,7 +20,7 @@ from ..domain_objects import ModelStatus, UpdateModelRequest, UpdateModelResponse from ..exception import InvalidStateTransitionError, ModelNotFoundError from .base_handler import BaseApiHandler -from .utils import to_lisa_model +from .utils import attach_guardrails_to_model, fetch_guardrails_for_model, to_lisa_model class UpdateModelHandler(BaseApiHandler): @@ -109,4 +109,10 @@ def __call__(self, model_id: str, update_request: UpdateModelRequest) -> UpdateM stateMachineArn=os.environ["UPDATE_SFN_ARN"], input=json.dumps(state_machine_payload) ) - return UpdateModelResponse(model=to_lisa_model(ddb_item)) + model = to_lisa_model(ddb_item) + + # Fetch and attach guardrails for this model + guardrail_items = fetch_guardrails_for_model(self._guardrails_table, model_id) + attach_guardrails_to_model(model, guardrail_items) + + return UpdateModelResponse(model=model) diff --git a/lambda/models/handler/utils.py b/lambda/models/handler/utils.py index db2b1519d..f45df0b4e 100644 --- a/lambda/models/handler/utils.py +++ b/lambda/models/handler/utils.py @@ -14,9 +14,9 @@ """Common utility functions across all API handlers.""" -from typing import Any, Dict +from typing import Any, Dict, List -from ..domain_objects import LISAModel +from ..domain_objects import GuardrailConfig, LISAModel def to_lisa_model(model_dict: Dict[str, Any]) -> LISAModel: @@ -26,3 +26,55 @@ def to_lisa_model(model_dict: Dict[str, Any]) -> LISAModel: model_dict["model_config"]["modelUrl"] = model_dict["model_url"] lisa_model: LISAModel = LISAModel.model_validate(model_dict["model_config"]) return lisa_model + + +def create_guardrail_config(item: Dict[str, Any]) -> GuardrailConfig: + """Create a GuardrailConfig object from a DynamoDB guardrail item.""" + return GuardrailConfig(**item) + + +def attach_guardrails_to_model(model: LISAModel, guardrail_items: List[Dict[str, Any]]) -> None: + """Build guardrails config from DDB items and attach to model.""" + if not guardrail_items: + return + + model.guardrailsConfig = { + f"guardrail-{item['guardrailName']}": create_guardrail_config(item) for item in guardrail_items + } + + +def fetch_guardrails_for_model(guardrails_table, model_id: str) -> List[Dict[str, Any]]: + """Query guardrails table for a specific model ID.""" + guardrails_response = guardrails_table.query( + IndexName="ModelIdIndex", + KeyConditionExpression="modelId = :modelId", + ExpressionAttributeValues={":modelId": model_id}, + ) + return guardrails_response.get("Items", []) + + +def fetch_all_guardrails(guardrails_table) -> List[Dict[str, Any]]: + """Scan all guardrails from the table with pagination.""" + all_guardrails = [] + guardrails_response = guardrails_table.scan() + all_guardrails.extend(guardrails_response.get("Items", [])) + pagination_key = guardrails_response.get("LastEvaluatedKey", None) + + while pagination_key: + guardrails_response = guardrails_table.scan(ExclusiveStartKey=pagination_key) + all_guardrails.extend(guardrails_response.get("Items", [])) + pagination_key = guardrails_response.get("LastEvaluatedKey", None) + + return all_guardrails + + +def group_guardrails_by_model(guardrail_items: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]: + """Group guardrail items by modelId.""" + guardrails_by_model: Dict[str, List[Dict[str, Any]]] = {} + for item in guardrail_items: + model_id = item["modelId"] + if model_id not in guardrails_by_model: + guardrails_by_model[model_id] = [] + guardrails_by_model[model_id].append(item) + + return guardrails_by_model diff --git a/lambda/models/lambda_functions.py b/lambda/models/lambda_functions.py index 034a89dd0..595b03655 100644 --- a/lambda/models/lambda_functions.py +++ b/lambda/models/lambda_functions.py @@ -57,6 +57,7 @@ dynamodb = boto3.resource("dynamodb", region_name=os.environ["AWS_REGION"], config=retry_config) iam_client = boto3.client("iam", region_name=os.environ["AWS_REGION"], config=retry_config) model_table = dynamodb.Table(os.environ["MODEL_TABLE_NAME"]) +guardrails_table = dynamodb.Table(os.environ["GUARDRAILS_TABLE_NAME"]) stepfunctions = boto3.client("stepfunctions", region_name=os.environ["AWS_REGION"], config=retry_config) @@ -92,6 +93,7 @@ async def create_model(create_request: CreateModelRequest) -> CreateModelRespons autoscaling_client=autoscaling, stepfunctions_client=stepfunctions, model_table_resource=model_table, + guardrails_table_resource=guardrails_table, ) return create_handler(create_request=create_request) @@ -104,6 +106,7 @@ async def list_models(request: Request) -> ListModelsResponse: autoscaling_client=autoscaling, stepfunctions_client=stepfunctions, model_table_resource=model_table, + guardrails_table_resource=guardrails_table, ) user_groups = [] @@ -130,6 +133,7 @@ async def get_model( autoscaling_client=autoscaling, stepfunctions_client=stepfunctions, model_table_resource=model_table, + guardrails_table_resource=guardrails_table, ) user_groups = [] @@ -157,6 +161,7 @@ async def update_model( autoscaling_client=autoscaling, stepfunctions_client=stepfunctions, model_table_resource=model_table, + guardrails_table_resource=guardrails_table, ) return update_handler(model_id=model_id, update_request=update_request) @@ -170,6 +175,7 @@ async def delete_model( autoscaling_client=autoscaling, stepfunctions_client=stepfunctions, model_table_resource=model_table, + guardrails_table_resource=guardrails_table, ) return delete_handler(model_id=model_id) diff --git a/lambda/models/state_machine/create_model.py b/lambda/models/state_machine/create_model.py index f70163b7b..365af01e7 100644 --- a/lambda/models/state_machine/create_model.py +++ b/lambda/models/state_machine/create_model.py @@ -24,7 +24,7 @@ import boto3 from botocore.config import Config from models.clients.litellm_client import LiteLLMClient -from models.domain_objects import CreateModelRequest, InferenceContainer, ModelStatus +from models.domain_objects import CreateModelRequest, GuardrailsTableEntry, InferenceContainer, ModelStatus from models.exception import ( MaxPollsExceededException, StackFailedToCreateException, @@ -46,6 +46,7 @@ ec2Client = boto3.client("ec2", region_name=os.environ["AWS_REGION"], config=retry_config) ddbResource = boto3.resource("dynamodb", region_name=os.environ["AWS_REGION"], config=retry_config) model_table = ddbResource.Table(os.environ["MODEL_TABLE_NAME"]) +guardrails_table = ddbResource.Table(os.environ["GUARDRAILS_TABLE_NAME"]) cfnClient = boto3.client("cloudformation", region_name=os.environ["AWS_REGION"], config=retry_config) iam_client = boto3.client("iam", region_name=os.environ["AWS_REGION"], config=retry_config) @@ -96,6 +97,11 @@ def handle_set_model_to_creating(event: Dict[str, Any], context: Any) -> Dict[st ) ) + # Create a copy of event without guardrailsConfig + # Guardrails are stored separately in the guardrails table + model_config_data = deepcopy(event) + model_config_data.pop("guardrailsConfig", None) + model_table.update_item( Key={"model_id": request.modelId}, UpdateExpression=( @@ -104,7 +110,7 @@ def handle_set_model_to_creating(event: Dict[str, Any], context: Any) -> Dict[st ), ExpressionAttributeValues={ ":model_status": ModelStatus.CREATING, - ":model_config": event, + ":model_config": model_config_data, ":model_description": request.modelDescription, ":lm": int(datetime.now(UTC).timestamp()), }, @@ -406,6 +412,108 @@ def handle_add_model_to_litellm(event: Dict[str, Any], context: Any) -> Dict[str return output_dict +def handle_add_guardrails_to_litellm(event: Dict[str, Any], context: Any) -> Dict[str, Any]: + """Add guardrails to LiteLLM and store them in DynamoDB.""" + logger.info(f"Adding guardrails to LiteLLM for model: {event.get('modelId')}") + output_dict = deepcopy(event) + + # Check if guardrails config exists + if not event.get("guardrailsConfig") or not event["guardrailsConfig"]: + logger.info("No guardrails configuration found, skipping guardrail creation") + output_dict["guardrail_ids"] = [] + return output_dict + + guardrail_ids = [] + created_guardrails = [] + + try: + # Process each guardrail in the configuration + for guardrail_key, guardrail_config in event["guardrailsConfig"].items(): + logger.info(f"Processing guardrail: {guardrail_key}") + + model_id = event["modelId"] + + # Transform guardrail config to LiteLLM format + litellm_guardrail_config = { + "guardrail": { + "guardrail_name": f'{guardrail_config["guardrailName"]}-{model_id}', + "litellm_params": { + "guardrail": "bedrock", + "mode": str(guardrail_config.get("mode", "pre_call")), + "guardrailIdentifier": guardrail_config["guardrailIdentifier"], + "guardrailVersion": guardrail_config.get("guardrailVersion", "DRAFT"), + "default_on": False, + }, + "guardrail_info": {"description": guardrail_config.get("description", "")}, + } + } + + # Create guardrail in LiteLLM + logger.info(f"Creating guardrail in LiteLLM: {guardrail_config['guardrailName']}") + litellm_response = litellm_client.create_guardrail(litellm_guardrail_config) + + # Extract LiteLLM guardrail ID from response + litellm_guardrail_id = None + if "guardrail_id" in litellm_response: + litellm_guardrail_id = litellm_response["guardrail_id"] + else: + logger.error(f"Unexpected LiteLLM guardrail response structure: {litellm_response}") + raise KeyError(f"Could not find guardrail ID in LiteLLM response: {litellm_response}") + + # Create guardrail entry for DynamoDB + guardrail_entry = GuardrailsTableEntry( + guardrailId=litellm_guardrail_id, + modelId=model_id, + guardrailName=guardrail_config["guardrailName"], + guardrailIdentifier=guardrail_config["guardrailIdentifier"], + guardrailVersion=guardrail_config.get("guardrailVersion", "DRAFT"), + mode=guardrail_config.get("mode", "pre_call"), + description=guardrail_config.get("description"), + allowedGroups=guardrail_config.get("allowedGroups", []), + ) + + # Store in DynamoDB + logger.info(f"Storing guardrail in DynamoDB: {litellm_guardrail_id}") + guardrails_table.put_item(Item=guardrail_entry.model_dump()) + + guardrail_ids.append(litellm_guardrail_id) + created_guardrails.append( + { + "guardrail_id": litellm_guardrail_id, + "guardrail_name": guardrail_config["guardrailName"], + } + ) + + logger.info( + f"Successfully created guardrail: {guardrail_config['guardrailName']} with ID: {litellm_guardrail_id}" + ) + + except Exception as e: + logger.error(f"Error creating guardrails: {str(e)}") + + # Clean up any created guardrails on failure + for created_guardrail in created_guardrails: + try: + logger.info(f"Cleaning up guardrail: {created_guardrail['guardrail_id']}") + # Delete from DynamoDB + guardrails_table.delete_item( + Key={"guardrail_id": created_guardrail["guardrail_id"], "model_id": event["modelId"]} + ) + # Delete from LiteLLM + litellm_client.delete_guardrail(created_guardrail["litellm_guardrail_id"]) + except Exception as cleanup_error: + logger.error(f"Error during guardrail cleanup: {str(cleanup_error)}") + + # Re-raise the original exception + raise e + + output_dict["guardrail_ids"] = guardrail_ids + output_dict["created_guardrails"] = created_guardrails + + logger.info(f"Successfully created {len(guardrail_ids)} guardrails for model: {event['modelId']}") + return output_dict + + def handle_failure(event: Dict[str, Any], context: Any) -> Dict[str, Any]: """ Handle failures from state machine. @@ -419,11 +527,32 @@ def handle_failure(event: Dict[str, Any], context: Any) -> Dict[str, Any]: to Failed. Cleaning up the CloudFormation stack, if it still exists, will happen in the DeleteModel API. """ logger.error(f"Handling state machine failure: {event}") - error_dict = json.loads( # error from SFN is json payload on top of json payload we add to the exception - json.loads(event["Cause"])["errorMessage"] - ) - error_reason = error_dict["error"] - original_event = error_dict["event"] + + try: + # Parse the error from Step Functions + cause_data = json.loads(event["Cause"]) + error_message = cause_data["errorMessage"] + + # Try to parse the error message as JSON (for our custom exceptions) + try: + error_dict = json.loads(error_message) + if isinstance(error_dict, dict) and "error" in error_dict: + error_reason = error_dict["error"] + original_event = error_dict.get("event", event) + else: + # If it's not our expected format, use the raw error message + error_reason = str(error_dict) if error_dict else "Unknown error" + original_event = event + except (json.JSONDecodeError, TypeError): + # If error_message is not JSON, use it directly + error_reason = error_message + original_event = event + + except (json.JSONDecodeError, KeyError, TypeError) as e: + logger.error(f"Error parsing failure event: {str(e)}") + error_reason = f"Failed to parse error details: {str(e)}" + original_event = event + logger.error(f"Failure reason: {error_reason}, Model ID: {original_event.get('modelId', 'unknown')}") # terminate EC2 instance if we have one recorded diff --git a/lambda/models/state_machine/delete_model.py b/lambda/models/state_machine/delete_model.py index e5f4ac9cc..e9707364b 100644 --- a/lambda/models/state_machine/delete_model.py +++ b/lambda/models/state_machine/delete_model.py @@ -34,6 +34,7 @@ cloudformation = boto3.client("cloudformation", region_name=os.environ["AWS_REGION"], config=retry_config) dynamodb = boto3.resource("dynamodb", region_name=os.environ["AWS_REGION"], config=retry_config) ddb_table = dynamodb.Table(os.environ["MODEL_TABLE_NAME"]) +guardrails_table = dynamodb.Table(os.environ["GUARDRAILS_TABLE_NAME"]) iam_client = boto3.client("iam", region_name=os.environ["AWS_REGION"], config=retry_config) secrets_manager = boto3.client("secretsmanager", region_name=os.environ["AWS_REGION"], config=retry_config) @@ -87,6 +88,68 @@ def handle_delete_from_litellm(event: Dict[str, Any], context: Any) -> Dict[str, return event +def handle_delete_guardrails(event: Dict[str, Any], context: Any) -> Dict[str, Any]: + """Delete all guardrails associated with the model from both LiteLLM and DynamoDB.""" + logger.info(f"Deleting guardrails for model: {event.get('modelId')}") + output_dict = deepcopy(event) + + model_id = event["modelId"] + deleted_guardrails = [] + + try: + # Get all guardrails for this model from DynamoDB + response = guardrails_table.query( + IndexName="ModelIdIndex", + KeyConditionExpression="modelId = :modelId", + ExpressionAttributeValues={":modelId": model_id}, + ) + + guardrails_to_delete = response.get("Items", []) + + if not guardrails_to_delete: + logger.info(f"No guardrails found for model: {model_id}") + output_dict["deleted_guardrails"] = [] + return output_dict + + logger.info(f"Found {len(guardrails_to_delete)} guardrails to delete for model: {model_id}") + + # Delete each guardrail from both LiteLLM and DynamoDB + for guardrail in guardrails_to_delete: + guardrail_id = guardrail["guardrailId"] + guardrail_name = guardrail["guardrailName"] + + try: + logger.info(f"Deleting guardrail from LiteLLM: {guardrail_name} (ID: {guardrail_id})") + # Delete from LiteLLM + litellm_client.delete_guardrail(guardrail_id) + + logger.info(f"Deleting guardrail from DynamoDB: {guardrail_name} (ID: {guardrail_id})") + # Delete from DynamoDB + guardrails_table.delete_item(Key={"guardrailId": guardrail_id, "modelId": model_id}) + + deleted_guardrails.append( + {"guardrail_id": guardrail_id, "guardrail_name": guardrail_name, "action": "deleted"} + ) + + logger.info(f"Successfully deleted guardrail: {guardrail_name}") + + except Exception as delete_error: + logger.error(f"Error deleting individual guardrail {guardrail_name}: {str(delete_error)}") + # Continue with other guardrails even if one fails + # We don't want to stop the entire model deletion process because of a guardrail deletion failure + continue + + except Exception as e: + logger.error(f"Error during guardrail deletion process for model {model_id}: {str(e)}") + # Don't raise the exception - we want to continue with model deletion even if guardrail cleanup fails + # Log the error but proceed with the deletion workflow + + output_dict["deleted_guardrails"] = deleted_guardrails + logger.info(f"Completed guardrail deletion for model: {model_id}. Deleted {len(deleted_guardrails)} guardrails.") + + return output_dict + + def handle_delete_stack(event: Dict[str, Any], context: Any) -> Dict[str, Any]: """Initialize stack deletion.""" stack_arn = event[CFN_STACK_ARN] diff --git a/lambda/models/state_machine/update_model.py b/lambda/models/state_machine/update_model.py index 7479cc49a..3978fcd39 100644 --- a/lambda/models/state_machine/update_model.py +++ b/lambda/models/state_machine/update_model.py @@ -23,11 +23,12 @@ import boto3 from models.clients.litellm_client import LiteLLMClient -from models.domain_objects import ModelStatus +from models.domain_objects import GuardrailsTableEntry, ModelStatus from utilities.common_functions import get_cert_path, get_rest_api_container_endpoint, retry_config ddbResource = boto3.resource("dynamodb", region_name=os.environ["AWS_REGION"], config=retry_config) model_table = ddbResource.Table(os.environ["MODEL_TABLE_NAME"]) +guardrails_table = ddbResource.Table(os.environ["GUARDRAILS_TABLE_NAME"]) autoscaling_client = boto3.client("autoscaling", region_name=os.environ["AWS_REGION"], config=retry_config) ecs_client = boto3.client("ecs", region_name=os.environ["AWS_REGION"], config=retry_config) cfn_client = boto3.client("cloudformation", region_name=os.environ["AWS_REGION"], config=retry_config) @@ -297,6 +298,9 @@ def handle_job_intake(event: Dict[str, Any], context: Any) -> Dict[str, Any]: has_metadata_update = has_metadata_update or is_autoscaling_update if has_metadata_update: + # Remove guardrailsConfig from model_config before storing + # Guardrails are stored separately in the guardrails table + model_config.pop("guardrailsConfig", None) ddb_update_expression += ", model_config = :mc" ddb_update_values[":mc"] = model_config @@ -320,10 +324,14 @@ def handle_job_intake(event: Dict[str, Any], context: Any) -> Dict[str, Any]: and model_status == ModelStatus.IN_SERVICE ) + # Determine if guardrails update is needed + needs_guardrails_update = event["update_payload"].get("guardrailsConfig") is not None + # We only need to poll for activation so that we know when to add the model back to LiteLLM output_dict["has_capacity_update"] = is_enable output_dict["is_disable"] = is_disable output_dict["needs_ecs_update"] = needs_ecs_update + output_dict["needs_guardrails_update"] = needs_guardrails_update output_dict["initial_model_status"] = model_status # needed for simple metadata updates output_dict["current_model_status"] = ddb_update_values[":ms"] # for state machine debugging / visibility @@ -433,6 +441,268 @@ def handle_finish_update(event: Dict[str, Any], context: Any) -> Dict[str, Any]: return output_dict +def handle_update_guardrails(event: Dict[str, Any], context: Any) -> Dict[str, Any]: + """ + Update guardrails for a model in LiteLLM and DynamoDB. + + This handler will: + 1. Process guardrails configuration updates from the event + 2. Update existing guardrails in LiteLLM + 3. Update guardrail entries in DynamoDB + 4. Handle creation of new guardrails and deletion of removed ones + """ + logger.info(f"Updating guardrails for model: {event.get('model_id')}") + output_dict = deepcopy(event) + + model_id = event["model_id"] + guardrails_config = event["update_payload"].get("guardrailsConfig") + + # Check if guardrails config exists + if not guardrails_config: + logger.info("No guardrails configuration found, skipping guardrail updates") + output_dict["guardrail_update_ids"] = [] + return output_dict + + updated_guardrails = [] + created_guardrails = [] + deleted_guardrails = [] + + try: + # Get existing guardrails for this model from DynamoDB + existing_guardrails = {} + response = guardrails_table.query( + IndexName="ModelIdIndex", + KeyConditionExpression="modelId = :modelId", + ExpressionAttributeValues={":modelId": model_id}, + ) + + for item in response.get("Items", []): + existing_guardrails[item["guardrailName"]] = item + + # Process each guardrail in the new configuration + processed_guardrail_names = set() + + for guardrail_config in guardrails_config.values(): + guardrail_name = guardrail_config["guardrailName"] + + logger.info(f"Processing guardrail update: {guardrail_name}") + + # Check if this guardrail is marked for deletion using deletion flag + if guardrail_config.get("markedForDeletion", False): + logger.info(f"Found guardrail marked for deletion: {guardrail_name}") + + # Add to processed names to prevent double deletion later + processed_guardrail_names.add(guardrail_name) + + # Find the existing guardrail to delete by name + guardrail_to_delete = existing_guardrails.get(guardrail_name) + + if guardrail_to_delete: + try: + logger.info( + f"Deleting guardrail: {guardrail_to_delete['guardrailName']} " + f"(ID: {guardrail_to_delete['guardrailId']})" + ) + + # Delete from LiteLLM + litellm_client.delete_guardrail(guardrail_to_delete["guardrailId"]) + + # Delete from DynamoDB + guardrails_table.delete_item( + Key={"guardrailId": guardrail_to_delete["guardrailId"], "modelId": model_id} + ) + + deleted_guardrails.append( + { + "guardrail_id": guardrail_to_delete["guardrailId"], + "guardrail_name": guardrail_to_delete["guardrailName"], + "action": "deleted", + } + ) + + logger.info(f"Successfully deleted guardrail: {guardrail_to_delete['guardrailName']}") + + except Exception as delete_error: + logger.error(f"Error deleting guardrail marked for deletion: {str(delete_error)}") + # Continue with other operations even if one deletion fails + else: + logger.warning(f"No matching guardrail found for deletion: {guardrail_name}") + + # Skip normal processing for deletion markers + continue + + processed_guardrail_names.add(guardrail_name) + + # Check if this is an existing guardrail or a new one + if guardrail_name in existing_guardrails: + # Update existing guardrail + existing_guardrail = existing_guardrails[guardrail_name] + litellm_guardrail_id = existing_guardrail["guardrailId"] + + # Transform guardrail config to LiteLLM format for update + litellm_guardrail_config = { + "guardrail": { + "guardrail_name": f'{guardrail_config["guardrailName"]}-{model_id}', + "litellm_params": { + "guardrail": "bedrock", + "mode": str(guardrail_config.get("mode", "pre_call")), + "guardrailIdentifier": guardrail_config["guardrailIdentifier"], + "guardrailVersion": guardrail_config.get("guardrailVersion", "DRAFT"), + "default_on": False, + }, + "guardrail_info": {"description": guardrail_config.get("description", "")}, + } + } + + # Update guardrail in LiteLLM + logger.info(f"Updating guardrail in LiteLLM: {guardrail_name}") + litellm_client.update_guardrail(litellm_guardrail_id, litellm_guardrail_config) + + # Update guardrail entry in DynamoDB + update_expression = ( + "SET guardrailIdentifier = :gi, guardrailVersion = :gv, #mode = :m, " + "description = :d, allowedGroups = :ag, lastModifiedDate = :lm" + ) + guardrails_table.update_item( + Key={"guardrailId": existing_guardrail["guardrailId"], "modelId": model_id}, + UpdateExpression=update_expression, + ExpressionAttributeNames={"#mode": "mode"}, # mode is a reserved keyword in DynamoDB + ExpressionAttributeValues={ + ":gi": guardrail_config["guardrailIdentifier"], + ":gv": guardrail_config.get("guardrailVersion", "DRAFT"), + ":m": str(guardrail_config.get("mode", "pre_call")), + ":d": guardrail_config.get("description"), + ":ag": guardrail_config.get("allowedGroups", []), + ":lm": int(datetime.now(UTC).timestamp() * 1000), + }, + ) + + updated_guardrails.append( + { + "guardrail_id": existing_guardrail["guardrailId"], + "guardrail_name": guardrail_name, + "action": "updated", + } + ) + + logger.info(f"Successfully updated guardrail: {guardrail_name}") + + else: + + # Transform guardrail config to LiteLLM format + litellm_guardrail_config = { + "guardrail": { + "guardrail_name": f'{guardrail_config["guardrailName"]}-{model_id}', + "litellm_params": { + "guardrail": "bedrock", + "mode": str(guardrail_config.get("mode", "pre_call")), + "guardrailIdentifier": guardrail_config["guardrailIdentifier"], + "guardrailVersion": guardrail_config.get("guardrailVersion", "DRAFT"), + "default_on": False, + }, + "guardrail_info": {"description": guardrail_config.get("description", "")}, + } + } + + # Create guardrail in LiteLLM + logger.info(f"Creating new guardrail in LiteLLM: {guardrail_name}") + litellm_response = litellm_client.create_guardrail(litellm_guardrail_config) + + # Extract LiteLLM guardrail ID from response + litellm_guardrail_id = None + if "guardrail_id" in litellm_response: + litellm_guardrail_id = litellm_response["guardrail_id"] + else: + logger.error(f"Unexpected LiteLLM guardrail response structure: {litellm_response}") + raise KeyError(f"Could not find guardrail ID in LiteLLM response: {litellm_response}") + + # Create guardrail entry for DynamoDB + guardrail_entry = GuardrailsTableEntry( + guardrailId=litellm_guardrail_id, + modelId=model_id, + guardrailName=guardrail_config["guardrailName"], + guardrailIdentifier=guardrail_config["guardrailIdentifier"], + guardrailVersion=guardrail_config.get("guardrailVersion", "DRAFT"), + mode=str(guardrail_config.get("mode", "pre_call")), + description=guardrail_config.get("description"), + allowedGroups=guardrail_config.get("allowedGroups", []), + ) + + # Store in DynamoDB + logger.info(f"Storing new guardrail in DynamoDB: {litellm_guardrail_id}") + guardrails_table.put_item(Item=guardrail_entry.model_dump()) + + created_guardrails.append( + {"guardrail_id": litellm_guardrail_id, "guardrail_name": guardrail_name, "action": "created"} + ) + + logger.info(f"Successfully created new guardrail: {guardrail_name}") + + # Delete guardrails that are no longer in the configuration + for guardrail_name, existing_guardrail in existing_guardrails.items(): + if guardrail_name not in processed_guardrail_names: + logger.info(f"Deleting removed guardrail: {guardrail_name}") + + try: + # Delete from LiteLLM + litellm_client.delete_guardrail(existing_guardrail["guardrailId"]) + + # Delete from DynamoDB + guardrails_table.delete_item( + Key={"guardrailId": existing_guardrail["guardrailId"], "modelId": model_id} + ) + + deleted_guardrails.append( + { + "guardrail_id": existing_guardrail["guardrailId"], + "guardrail_name": guardrail_name, + "action": "deleted", + } + ) + + logger.info(f"Successfully deleted guardrail: {guardrail_name}") + + except Exception as delete_error: + logger.error(f"Error deleting guardrail {guardrail_name}: {str(delete_error)}") + # Continue with other operations even if one deletion fails + + # Combine all operations for output + all_guardrail_operations = updated_guardrails + created_guardrails + deleted_guardrails + + except Exception as e: + logger.error(f"Error updating guardrails: {str(e)}") + + # Clean up any newly created guardrails on failure + for created_guardrail in created_guardrails: + try: + logger.info(f"Cleaning up created guardrail: {created_guardrail['guardrail_id']}") + # Delete from DynamoDB + guardrails_table.delete_item( + Key={"guardrailId": created_guardrail["guardrail_id"], "modelId": model_id} + ) + # Delete from LiteLLM + litellm_client.delete_guardrail(created_guardrail["guardrail_id"]) + except Exception as cleanup_error: + logger.error(f"Error during guardrail cleanup: {str(cleanup_error)}") + + # Re-raise the original exception + raise e + + output_dict["guardrail_updates"] = all_guardrail_operations + output_dict["guardrail_update_summary"] = { + "updated": len(updated_guardrails), + "created": len(created_guardrails), + "deleted": len(deleted_guardrails), + } + + logger.info( + f"Successfully processed guardrail updates for model: {model_id}. " + f"Updated: {len(updated_guardrails)}, Created: {len(created_guardrails)}, " + f"Deleted: {len(deleted_guardrails)}" + ) + return output_dict + + def get_ecs_resources_from_stack(stack_name: str) -> tuple[str, str, str]: """Extract ECS service name, cluster name, and current task definition ARN from CloudFormation.""" try: diff --git a/lib/models/guardrails-table.ts b/lib/models/guardrails-table.ts new file mode 100644 index 000000000..c1b211454 --- /dev/null +++ b/lib/models/guardrails-table.ts @@ -0,0 +1,73 @@ +/** + 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 { AttributeType, BillingMode, Table, TableEncryption } from 'aws-cdk-lib/aws-dynamodb'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { Construct } from 'constructs'; + +/** + * Properties for GuardrailsTable Construct. + */ +export type GuardrailsTableProps = { + deploymentPrefix: string; + removalPolicy: any; +}; + +/** + * DynamoDB table for storing Bedrock Guardrails configurations per model + */ +export class GuardrailsTable extends Construct { + public readonly table: Table; + + constructor (scope: Construct, id: string, props: GuardrailsTableProps) { + super(scope, id); + + const { deploymentPrefix, removalPolicy } = props; + + // Create the guardrails table with composite key structure + this.table = new Table(this, 'GuardrailsTable', { + partitionKey: { + name: 'guardrailId', + type: AttributeType.STRING + }, + sortKey: { + name: 'modelId', + type: AttributeType.STRING + }, + billingMode: BillingMode.PAY_PER_REQUEST, + encryption: TableEncryption.AWS_MANAGED, + removalPolicy: removalPolicy, + }); + + this.table.addGlobalSecondaryIndex({ + indexName: 'ModelIdIndex', + partitionKey: { + name: 'modelId', + type: AttributeType.STRING + }, + sortKey: { + name: 'guardrailId', + type: AttributeType.STRING + }, + }); + + // Create SSM parameter for guardrails table name + new StringParameter(this, 'GuardrailsTableNameParameter', { + parameterName: `${deploymentPrefix}/guardrailsTableName`, + stringValue: this.table.tableName, + }); + } +} diff --git a/lib/models/model-api.ts b/lib/models/model-api.ts index 0ec602a99..4922ecc1a 100644 --- a/lib/models/model-api.ts +++ b/lib/models/model-api.ts @@ -49,6 +49,7 @@ import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; import { createCdkId, createLambdaRole } from '../core/utils'; import { Roles } from '../core/iam/roles'; import { LAMBDA_PATH } from '../util'; +import { GuardrailsTable } from './guardrails-table'; /** * Properties for ModelsApi Construct. @@ -126,6 +127,12 @@ export class ModelsApi extends Construct { stringValue: modelTable.tableName, }); + // Create guardrails table + const guardrailsTable = new GuardrailsTable(this, 'GuardrailsTable', { + deploymentPrefix: config.deploymentPrefix || '', + removalPolicy: config.removalPolicy, + }); + const ecsModelBuildRepo = new Repository(this, 'ecs-model-build-repo'); const ecsModelDeployer = new ECSModelDeployer(this, 'ecs-model-deployer', { @@ -150,12 +157,13 @@ export class ModelsApi extends Construct { const stateMachinesLambdaRole = config.roles ? Role.fromRoleName(this, Roles.MODEL_SFN_LAMBDA_ROLE, config.roles.ModelsSfnLambdaRole) : - this.createStateMachineLambdaRole(modelTable.tableArn, dockerImageBuilder.dockerImageBuilderFn.functionArn, + this.createStateMachineLambdaRole(modelTable.tableArn, guardrailsTable.table.tableArn, dockerImageBuilder.dockerImageBuilderFn.functionArn, ecsModelDeployer.ecsModelDeployerFn.functionArn, lisaServeEndpointUrlPs.parameterArn, managementKeyName, config); const createModelStateMachine = new CreateModelStateMachine(this, 'CreateModelWorkflow', { config: config, modelTable: modelTable, + guardrailsTable: guardrailsTable.table, lambdaLayers: lambdaLayers, role: stateMachinesLambdaRole, vpc: vpc, @@ -171,6 +179,7 @@ export class ModelsApi extends Construct { const deleteModelStateMachine = new DeleteModelStateMachine(this, 'DeleteModelWorkflow', { config: config, modelTable: modelTable, + guardrailsTable: guardrailsTable.table, lambdaLayers: lambdaLayers, role: stateMachinesLambdaRole, vpc: vpc, @@ -183,6 +192,7 @@ export class ModelsApi extends Construct { const updateModelStateMachine = new UpdateModelStateMachine(this, 'UpdateModelWorkflow', { config: config, modelTable: modelTable, + guardrailsTable: guardrailsTable.table, lambdaLayers: lambdaLayers, role: stateMachinesLambdaRole, vpc: vpc, @@ -200,6 +210,7 @@ export class ModelsApi extends Construct { DELETE_SFN_ARN: deleteModelStateMachine.stateMachineArn, UPDATE_SFN_ARN: updateModelStateMachine.stateMachineArn, MODEL_TABLE_NAME: modelTable.tableName, + GUARDRAILS_TABLE_NAME: guardrailsTable.table.tableName, }; const lambdaRole: IRole = createLambdaRole(this, config.deploymentName, 'ModelApi', modelTable.tableArn, config.roles?.ModelApiRole); @@ -322,6 +333,21 @@ export class ModelsApi extends Construct { `${modelTable.tableArn}/*` ], }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'dynamodb:GetItem', + 'dynamodb:PutItem', + 'dynamodb:UpdateItem', + 'dynamodb:DeleteItem', + 'dynamodb:Query', + 'dynamodb:Scan', + ], + resources: [ + guardrailsTable.table.tableArn, + `${guardrailsTable.table.tableArn}/*` + ], + }), new PolicyStatement({ effect: Effect.ALLOW, actions: [ @@ -376,7 +402,7 @@ export class ModelsApi extends Construct { * @param managementKeyName - Name of the management key secret * @returns The created role */ - createStateMachineLambdaRole (modelTableArn: string, dockerImageBuilderFnArn: string, ecsModelDeployerFnArn: string, lisaServeEndpointUrlParamArn: string, managementKeyName: string, config: any): IRole { + createStateMachineLambdaRole (modelTableArn: string, guardrailTableArn: string ,dockerImageBuilderFnArn: string, ecsModelDeployerFnArn: string, lisaServeEndpointUrlParamArn: string, managementKeyName: string, config: any): IRole { return new Role(this, Roles.MODEL_SFN_LAMBDA_ROLE, { assumedBy: new ServicePrincipal('lambda.amazonaws.com'), managedPolicies: [ @@ -393,10 +419,13 @@ export class ModelsApi extends Construct { 'dynamodb:PutItem', 'dynamodb:UpdateItem', 'dynamodb:Scan', + 'dynamodb:Query' ], resources: [ modelTableArn, `${modelTableArn}/*`, + guardrailTableArn, + `${guardrailTableArn}/*`, ] }), new PolicyStatement({ @@ -498,6 +527,7 @@ export class ModelsApi extends Construct { actions: [ 'bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream', + 'bedrock:ApplyGuardrail' ], resources: ['*'], // Bedrock model ARNs are dynamic and region-specific }), diff --git a/lib/models/state-machine/create-model.ts b/lib/models/state-machine/create-model.ts index 57c7a3d4c..a71675586 100644 --- a/lib/models/state-machine/create-model.ts +++ b/lib/models/state-machine/create-model.ts @@ -40,6 +40,7 @@ import { LAMBDA_PATH } from '../../util'; type CreateModelStateMachineProps = BaseProps & { modelTable: ITable, + guardrailsTable: ITable, lambdaLayers: ILayerVersion[]; dockerImageBuilderFnArn: string; ecsModelDeployerFnArn: string; @@ -70,6 +71,7 @@ export class CreateModelStateMachine extends Construct { ECS_MODEL_DEPLOYER_FN_ARN: ecsModelDeployerFnArn, LISA_API_URL_PS_NAME: restApiContainerEndpointPs.parameterName, MODEL_TABLE_NAME: modelTable.tableName, + GUARDRAILS_TABLE_NAME: props.guardrailsTable.tableName, REST_API_VERSION: 'v2', MANAGEMENT_KEY_NAME: managementKeyName, RESTAPI_SSL_CERT_ARN: config.restApiConfig?.sslCertIamArn ?? '', @@ -211,12 +213,32 @@ export class CreateModelStateMachine extends Construct { outputPath: OUTPUT_PATH, }); + const addGuardrailsToLitellm = new LambdaInvoke(this, 'AddGuardrailsToLitellm', { + lambdaFunction: new Function(this, 'AddGuardrailsToLitellmFunc', { + runtime: getDefaultRuntime(), + handler: 'models.state_machine.create_model.handle_add_guardrails_to_litellm', + code: Code.fromAsset(lambdaPath), + timeout: LAMBDA_TIMEOUT, + memorySize: LAMBDA_MEMORY, + role: role, + vpc: vpc.vpc, + vpcSubnets: vpc.subnetSelection, + securityGroups: securityGroups, + layers: lambdaLayers, + environment: environment, + }), + outputPath: OUTPUT_PATH, + }); + const successState = new Succeed(this, 'CreateSuccess'); const failState = new Fail(this, 'CreateFailed'); // Check if image is pre-existing ECR image const checkImageTypeChoice = new Choice(this, 'CheckImageTypeChoice'); + // Check if guardrails need to be added + const checkGuardrailsChoice = new Choice(this, 'CheckGuardrailsChoice'); + // State Machine definition setModelToCreating.next(createModelInfraChoice); createModelInfraChoice @@ -259,9 +281,18 @@ export class CreateModelStateMachine extends Construct { .otherwise(addModelToLitellm); waitBeforePollingCreateStack.next(pollCreateStack); + // Check for guardrails and add them if present + addModelToLitellm.next(checkGuardrailsChoice); + checkGuardrailsChoice + .when(Condition.isPresent('$.guardrailsConfig'), addGuardrailsToLitellm) + .otherwise(successState); + // terminal states handleFailureState.next(failState); - addModelToLitellm.next(successState); + addGuardrailsToLitellm.next(successState); + addGuardrailsToLitellm.addCatch(handleFailureState, { // fail if guardrail creation fails + errors: ['States.TaskFailed'], + }); const stateMachine = new StateMachine(this, 'CreateModelSM', { definitionBody: DefinitionBody.fromChainable(setModelToCreating), diff --git a/lib/models/state-machine/delete-model.ts b/lib/models/state-machine/delete-model.ts index 03c4b9bb1..3f773b8f0 100644 --- a/lib/models/state-machine/delete-model.ts +++ b/lib/models/state-machine/delete-model.ts @@ -38,6 +38,7 @@ import { LAMBDA_PATH } from '../../util'; type DeleteModelStateMachineProps = BaseProps & { modelTable: ITable, + guardrailsTable: ITable, lambdaLayers: ILayerVersion[], vpc: Vpc, securityGroups: ISecurityGroup[]; @@ -57,10 +58,11 @@ export class DeleteModelStateMachine extends Construct { constructor (scope: Construct, id: string, props: DeleteModelStateMachineProps) { super(scope, id); - const { config, modelTable, lambdaLayers, role, vpc, securityGroups, restApiContainerEndpointPs, managementKeyName, executionRole } = props; + const { config, modelTable, guardrailsTable, lambdaLayers, role, vpc, securityGroups, restApiContainerEndpointPs, managementKeyName, executionRole } = props; const environment = { // Environment variables to set in all Lambda functions MODEL_TABLE_NAME: modelTable.tableName, + GUARDRAILS_TABLE_NAME: guardrailsTable.tableName, LISA_API_URL_PS_NAME: restApiContainerEndpointPs.parameterName, REST_API_VERSION: 'v2', MANAGEMENT_KEY_NAME: managementKeyName, @@ -154,6 +156,23 @@ export class DeleteModelStateMachine extends Construct { outputPath: OUTPUT_PATH, }); + const deleteGuardrails = new LambdaInvoke(this, 'DeleteGuardrails', { + lambdaFunction: new Function(this, 'DeleteGuardrailsFunc', { + runtime: getDefaultRuntime(), + handler: 'models.state_machine.delete_model.handle_delete_guardrails', + code: Code.fromAsset(lambdaPath), + timeout: LAMBDA_TIMEOUT, + memorySize: LAMBDA_MEMORY, + role: role, + vpc: vpc.vpc, + vpcSubnets: vpc.subnetSelection, + securityGroups: securityGroups, + layers: lambdaLayers, + environment: environment, + }), + outputPath: OUTPUT_PATH, + }); + const successState = new Succeed(this, 'DeleteSuccess'); const deleteStackChoice = new Choice(this, 'DeleteStackChoice'); @@ -164,7 +183,8 @@ export class DeleteModelStateMachine extends Construct { // State Machine definition setModelToDeleting.next(deleteFromLitellm); - deleteFromLitellm.next(deleteStackChoice); + deleteFromLitellm.next(deleteGuardrails); + deleteGuardrails.next(deleteStackChoice); deleteStackChoice .when(Condition.isNotNull('$.cloudformation_stack_arn'), deleteStack) diff --git a/lib/models/state-machine/update-model.ts b/lib/models/state-machine/update-model.ts index 1c2b3c0dc..79c36eca6 100644 --- a/lib/models/state-machine/update-model.ts +++ b/lib/models/state-machine/update-model.ts @@ -39,6 +39,7 @@ import { LAMBDA_PATH } from '../../util'; type UpdateModelStateMachineProps = BaseProps & { modelTable: ITable, + guardrailsTable: ITable, lambdaLayers: ILayerVersion[], vpc: Vpc, securityGroups: ISecurityGroup[]; @@ -72,6 +73,7 @@ export class UpdateModelStateMachine extends Construct { const environment = { // Environment variables to set in all Lambda functions MODEL_TABLE_NAME: modelTable.tableName, + GUARDRAILS_TABLE_NAME: props.guardrailsTable.tableName, LISA_API_URL_PS_NAME: restApiContainerEndpointPs.parameterName, REST_API_VERSION: 'v2', MANAGEMENT_KEY_NAME: managementKeyName, @@ -147,6 +149,23 @@ export class UpdateModelStateMachine extends Construct { outputPath: OUTPUT_PATH, }); + const handleUpdateGuardrails = new LambdaInvoke(this, 'HandleUpdateGuardrails', { + lambdaFunction: new Function(this, 'HandleUpdateGuardrailsFunc', { + runtime: getDefaultRuntime(), + handler: 'models.state_machine.update_model.handle_update_guardrails', + code: Code.fromAsset(lambdaPath), + timeout: LAMBDA_TIMEOUT, + memorySize: LAMBDA_MEMORY, + role: role, + vpc: vpc.vpc, + vpcSubnets: vpc.subnetSelection, + securityGroups: securityGroups, + layers: lambdaLayers, + environment: environment, + }), + outputPath: OUTPUT_PATH, + }); + const handleFinishUpdate = new LambdaInvoke(this, 'HandleFinishUpdate', { lambdaFunction: new Function(this, 'HandleFinishUpdateFunc', { runtime: getDefaultRuntime(), @@ -169,6 +188,7 @@ export class UpdateModelStateMachine extends Construct { // choice states const hasEcsUpdateChoice = new Choice(this, 'HasEcsUpdateChoice'); + const hasGuardrailsUpdateChoice = new Choice(this, 'HasGuardrailsUpdateChoice'); const hasCapacityUpdateChoice = new Choice(this, 'HasCapacityUpdateChoice'); const pollAsgChoice = new Choice(this, 'PollAsgChoice'); const pollEcsDeploymentChoice = new Choice(this, 'PollEcsDeploymentChoice'); @@ -190,15 +210,22 @@ export class UpdateModelStateMachine extends Construct { // ECS update flow hasEcsUpdateChoice .when(Condition.booleanEquals('$.needs_ecs_update', true), handleEcsUpdate) - .otherwise(hasCapacityUpdateChoice); + .otherwise(hasGuardrailsUpdateChoice); handleEcsUpdate.next(handlePollEcsDeployment); handlePollEcsDeployment.next(pollEcsDeploymentChoice); pollEcsDeploymentChoice .when(Condition.booleanEquals('$.should_continue_ecs_polling', true), waitBeforePollEcsDeployment) - .otherwise(hasCapacityUpdateChoice); + .otherwise(hasGuardrailsUpdateChoice); waitBeforePollEcsDeployment.next(handlePollEcsDeployment); + // Guardrails update flow + hasGuardrailsUpdateChoice + .when(Condition.booleanEquals('$.needs_guardrails_update', true), handleUpdateGuardrails) + .otherwise(hasCapacityUpdateChoice); + + handleUpdateGuardrails.next(hasCapacityUpdateChoice); + // Existing capacity update flow hasCapacityUpdateChoice .when(Condition.booleanEquals('$.has_capacity_update', true), handlePollCapacity) diff --git a/lib/serve/rest-api/src/api/endpoints/v2/litellm_passthrough.py b/lib/serve/rest-api/src/api/endpoints/v2/litellm_passthrough.py index 1341b3469..70f28f488 100644 --- a/lib/serve/rest-api/src/api/endpoints/v2/litellm_passthrough.py +++ b/lib/serve/rest-api/src/api/endpoints/v2/litellm_passthrough.py @@ -14,6 +14,7 @@ """Model invocation routes.""" +import json import logging import os from collections.abc import Iterator @@ -25,7 +26,15 @@ from fastapi.responses import JSONResponse, Response, StreamingResponse from starlette.status import HTTP_401_UNAUTHORIZED -from ....auth import Authorizer +from ....auth import Authorizer, extract_user_groups_from_jwt +from ....utils.guardrails import ( + create_guardrail_json_response, + create_guardrail_streaming_response, + extract_guardrail_response, + get_applicable_guardrails, + get_model_guardrails, + is_guardrail_violation, +) # Local LiteLLM installation URL. By default, LiteLLM runs on port 4000. Change the port here if the # port was changed as part of the LiteLLM startup in entrypoint.sh @@ -81,6 +90,94 @@ router = APIRouter() +async def apply_guardrails_to_request(params: dict, model_id: str, jwt_data: dict) -> None: + """ + Apply guardrails to a chat completion request. + + This function modifies the params dict in-place, adding applicable guardrails + based on the user's group membership and the model's guardrail configuration. + + Args: + params: The request parameters dict to modify + model_id: The model ID to get guardrails for + jwt_data: JWT data containing user information + + Raises: + No exceptions are raised - errors are logged and the request continues + """ + try: + # Get guardrails for this model + guardrails = await get_model_guardrails(model_id) + + if not guardrails: + return + + # Extract user groups from JWT + user_groups = extract_user_groups_from_jwt(jwt_data) + + # Determine which guardrails apply to this user + applicable_guardrail_names = get_applicable_guardrails(user_groups, guardrails, model_id) + + # Add guardrails to request if any apply + if applicable_guardrail_names: + params["guardrails"] = applicable_guardrail_names + logger.info(f"Applying guardrails to model {model_id}: {applicable_guardrail_names}") + + except Exception as e: + logger.error(f"Error applying guardrails for model {model_id}: {e}") + # Continue with request even if guardrails fail to apply + + +def handle_guardrail_violation_response( + response: requests.Response, model_id: str, params: dict, is_streaming: bool +) -> Response | None: + """ + Handle guardrail violation errors in LiteLLM responses. + + Checks if a 400 error response contains a guardrail violation and converts it + into an appropriate format (streaming or non-streaming). + + Args: + response: The HTTP response from LiteLLM + model_id: The model ID from the request + params: The original request parameters + is_streaming: Whether this is a streaming request + + Returns: + Response object if a guardrail violation was handled, None otherwise + """ + if response.status_code != 400: + return None + + try: + error_response = response.json() + error_msg = error_response.get("error", {}).get("message", "") + + if not is_guardrail_violation(error_msg): + return None + + logger.info("Guardrail policy violated") + + guardrail_response = extract_guardrail_response(error_msg) + if not guardrail_response: + return None + + created = int(error_response.get("created", 0) if is_streaming else params.get("created", 0)) + + if is_streaming: + # Return as streaming response + return StreamingResponse( + create_guardrail_streaming_response(guardrail_response, model_id, created), status_code=200 + ) + else: + # Return as a normal completion response + return create_guardrail_json_response(guardrail_response, model_id, created) + + except Exception as e: + logger.error(f"Error handling guardrail violation: {e}") + return None + + def generate_response(iterator: Iterator[Union[str, bytes]]) -> Iterator[str]: """For streaming responses, generate strings instead of bytes objects so that clients recognize the LLM output.""" for line in iterator: @@ -90,6 +187,66 @@ def generate_response(iterator: Iterator[Union[str, bytes]]) -> Iterator[str]: yield f"{line}\n\n" +def generate_response_with_guardrail_handling(iterator: Iterator[Union[str, bytes]], model: str) -> Iterator[str]: + """ + Generate streaming responses with guardrail violation error handling. + + This wrapper checks each chunk in the stream for guardrail violations and converts + them into properly formatted streaming responses. + """ + for line in iterator: + if isinstance(line, bytes): + line = line.decode() + + if not line: + continue + + # Check if this line contains an error (SSE format: "data: {...}") + if line.startswith("data: "): + try: + # Extract JSON from SSE data line + data_content = line[6:].strip() # Remove "data: " prefix + + # Skip [DONE] marker + if data_content == "[DONE]": + yield f"{line}\n\n" + continue + + # Try to parse as JSON to check for errors + chunk_data = json.loads(data_content) + + # Check if this is an error chunk + if "error" in chunk_data: + error_msg = chunk_data.get("error", {}).get("message", "") + + if is_guardrail_violation(error_msg): + logger.info("Guardrail policy violated in streaming response") + + guardrail_response = extract_guardrail_response(error_msg) + if guardrail_response: + # Stream the guardrail response + created = int(chunk_data.get("created", 0)) + for chunk in create_guardrail_streaming_response(guardrail_response, model, created): + yield chunk + return # Stop streaming after guardrail response + else: + # Could not extract guardrail response, pass through the error + yield f"{line}\n\n" + else: + # Different error, pass it through + yield f"{line}\n\n" + else: + # Normal chunk, pass it through + yield f"{line}\n\n" + + except json.JSONDecodeError: + # Not valid JSON or not in expected format, pass through as-is + yield f"{line}\n\n" + else: + # Not in SSE format, pass through as-is + yield f"{line}\n\n" + + @router.api_route("/{api_path:path}", methods=["GET", "POST", "OPTIONS", "PUT", "PATCH", "DELETE", "HEAD"]) async def litellm_passthrough(request: Request, api_path: str) -> Response: """ @@ -104,6 +261,7 @@ async def litellm_passthrough(request: Request, api_path: str) -> Response: authorizer = Authorizer() require_admin = api_path not in OPENAI_ROUTES + jwt_data = await authorizer.authenticate_request(request) if not await authorizer.can_access(request, require_admin): raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Not authenticated in litellm_passthrough") @@ -114,16 +272,46 @@ async def litellm_passthrough(request: Request, api_path: str) -> Response: headers["Authorization"] = f"Bearer {LITELLM_KEY}" http_method = request.method - if http_method == "GET": + if http_method == "GET" or http_method == "DELETE": response = requests.request(method=http_method, url=litellm_path, headers=headers) return JSONResponse(response.json(), status_code=response.status_code) - # not a GET request, so expect a JSON payload as part of the request + # not a GET or DELETE request, so expect a JSON payload as part of the request params = await request.json() + + # Apply guardrails for chat/completions requests + if api_path in ["chat/completions", "v1/chat/completions"]: + model_id = params.get("model") + if model_id: + await apply_guardrails_to_request(params, model_id, jwt_data) + if params.get("stream", False): # if a streaming request response = requests.request(method=http_method, url=litellm_path, json=params, headers=headers, stream=True) - return StreamingResponse(generate_response(response.iter_lines()), status_code=response.status_code) + + # Check for guardrail violations + model_id = params.get("model", "") + guardrail_response = handle_guardrail_violation_response(response, model_id, params, is_streaming=True) + if guardrail_response: + return guardrail_response + + # Normal streaming (no error or non-guardrail error) + # Use guardrail-aware generator for chat/completions endpoints + if api_path in ["chat/completions", "v1/chat/completions"]: + model_id = params.get("model", "") + return StreamingResponse( + generate_response_with_guardrail_handling(response.iter_lines(), model_id), + status_code=response.status_code, + ) + else: + return StreamingResponse(generate_response(response.iter_lines()), status_code=response.status_code) else: # not a streaming request response = requests.request(method=http_method, url=litellm_path, json=params, headers=headers) + + # Check for guardrail violations + model_id = params.get("model", "") + guardrail_response = handle_guardrail_violation_response(response, model_id, params, is_streaming=False) + if guardrail_response: + return guardrail_response + if response.status_code != 200: logger.error(f"LiteLLM error response: {response.text}") return JSONResponse(response.json(), status_code=response.status_code) diff --git a/lib/serve/rest-api/src/auth.py b/lib/serve/rest-api/src/auth.py index 07f3bc07f..2f36552d1 100644 --- a/lib/serve/rest-api/src/auth.py +++ b/lib/serve/rest-api/src/auth.py @@ -132,6 +132,50 @@ def is_user_in_group(jwt_data: dict[str, Any], group: str, jwt_groups_property: return group in current_node +def extract_user_groups_from_jwt(jwt_data: Optional[Dict[str, Any]]) -> list[str]: + """ + Extract user groups from JWT data using the JWT_GROUPS_PROP environment variable. + + This follows the same property path traversal logic as is_user_in_group() function. + + Parameters + ---------- + jwt_data : Optional[Dict[str, Any]] + JWT data from authentication. None if user authenticated via API token. + + Returns + ------- + list[str] + List of groups the user belongs to. Empty list if no JWT data or groups not found. + """ + if jwt_data is None: + # API token users have no JWT, treat as having no group restrictions + return [] + + jwt_groups_property = os.environ.get("JWT_GROUPS_PROP", "") + if not jwt_groups_property: + logger.warning("JWT_GROUPS_PROP environment variable not set") + return [] + + # Traverse the property path to find groups + props = jwt_groups_property.split(".") + current_node = jwt_data + + for prop in props: + if isinstance(current_node, dict) and prop in current_node: + current_node = current_node[prop] + else: + logger.debug(f"Groups property path '{jwt_groups_property}' not found in JWT data") + return [] + + # current_node should now be the groups list + if isinstance(current_node, list): + return current_node + else: + logger.warning(f"Expected list of groups but got {type(current_node)}") + return [] + + def get_authorization_token(headers: Dict[str, str], header_name: str = AuthHeaders.AUTHORIZATION) -> str: """Get Bearer token from Authorization headers if it exists.""" if header_name in headers: diff --git a/lib/serve/rest-api/src/utils/guardrails.py b/lib/serve/rest-api/src/utils/guardrails.py new file mode 100644 index 000000000..1d6eb042a --- /dev/null +++ b/lib/serve/rest-api/src/utils/guardrails.py @@ -0,0 +1,239 @@ +# 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. + +"""Utilities for managing and applying LiteLLM guardrails.""" + +import json +import os +import re +from collections.abc import Iterator +from typing import Any, Dict, List, Optional + +import boto3 +from fastapi.responses import JSONResponse +from loguru import logger + + +async def get_model_guardrails(model_id: str) -> List[Dict[str, Any]]: + """ + Query the guardrails DynamoDB table for guardrails associated with a model. + + Parameters + ---------- + model_id : str + The model ID to query guardrails for. + + Returns + ------- + List[Dict[str, Any]] + List of guardrail configurations for the model. Returns empty list if no guardrails found. + """ + try: + dynamodb = boto3.resource("dynamodb", region_name=os.environ["AWS_REGION"]) + guardrails_table = dynamodb.Table(os.environ["GUARDRAILS_TABLE_NAME"]) + + # Query using the ModelIdIndex GSI + response = guardrails_table.query( + IndexName="ModelIdIndex", + KeyConditionExpression="modelId = :modelId", + ExpressionAttributeValues={":modelId": model_id}, + ) + + guardrails = response.get("Items", []) + logger.debug(f"Found {len(guardrails)} guardrails for model {model_id}") + return guardrails + + except Exception as e: + logger.error(f"Error fetching guardrails for model {model_id}: {e}") + return [] + + +def get_applicable_guardrails(user_groups: List[str], guardrails: List[Dict[str, Any]], model_id: str) -> List[str]: + """ + Determine which guardrails apply to a user based on group membership. + + A guardrail applies if: + - It has no allowed_groups (public guardrail, applies to everyone) + - The user is a member of at least one of the guardrail's allowed_groups + + Parameters + ---------- + user_groups : List[str] + List of groups the user belongs to. + + guardrails : List[Dict[str, Any]] + List of guardrail configurations from DynamoDB. + + model_id : str + The model ID being invoked. Used to construct the full LiteLLM guardrail name. + + Returns + ------- + List[str] + List of LiteLLM guardrail names (format: {guardrail_name}-{model_id}) that should be applied to the request. + """ + applicable_guardrails = [] + + for guardrail in guardrails: + # Skip guardrails marked for deletion + if guardrail.get("markedForDeletion", False): + continue + + allowed_groups = guardrail.get("allowedGroups", []) + guardrail_name = guardrail.get("guardrailName") + + if not guardrail_name: + logger.warning(f"Guardrail missing guardrailName field: {guardrail}") + continue + + # Construct the full LiteLLM guardrail name (matches format used in create_model.py) + litellm_guardrail_name = f"{guardrail_name}-{model_id}" + + # If no groups specified, guardrail is public (applies to everyone) + if not allowed_groups: + applicable_guardrails.append(litellm_guardrail_name) + logger.debug(f"Applying public guardrail: {litellm_guardrail_name}") + continue + + # Check if user has any matching group + if any(group in allowed_groups for group in user_groups): + applicable_guardrails.append(litellm_guardrail_name) + logger.debug(f"Applying guardrail {litellm_guardrail_name} based on group membership") + + return applicable_guardrails + + +def is_guardrail_violation(error_msg: str) -> bool: + """ + Check if an error message indicates a guardrail policy violation. + + Parameters + ---------- + error_msg : str + The error message to check. + + Returns + ------- + bool + True if the error message indicates a guardrail violation, False otherwise. + """ + return "Violated guardrail policy" in error_msg + + +def extract_guardrail_response(error_msg: str) -> Optional[str]: + """ + Extract the bedrock_guardrail_response from an error message. + + Parameters + ---------- + error_msg : str + The error message containing the guardrail response. + + Returns + ------- + Optional[str] + The extracted guardrail response text, or None if not found. + """ + match = re.search(r"'bedrock_guardrail_response':\s*'([^']*)'", error_msg) + return match.group(1) if match else None + + +def create_guardrail_streaming_response(guardrail_response: str, model_id: str, created: int = 0) -> Iterator[str]: + """ + Generate streaming response chunks for a guardrail violation. + + Parameters + ---------- + guardrail_response : str + The guardrail response text to stream. + model_id : str + The model ID associated with the request. + created : int, optional + The creation timestamp, by default 0. + + Yields + ------ + str + Properly formatted SSE chunks for the guardrail response. + """ + # First chunk with content + response_chunk = { + "id": "guardrail-response", + "object": "chat.completion.chunk", + "created": created, + "model": model_id, + "choices": [ + { + "index": 0, + "delta": {"role": "assistant", "content": guardrail_response}, + "finish_reason": None, + } + ], + "lisa_guardrail_triggered": True, + } + yield f"data: {json.dumps(response_chunk)}\n\n" + + # Final chunk with finish_reason + final_chunk = { + "id": "guardrail-response", + "object": "chat.completion.chunk", + "created": created, + "model": model_id, + "choices": [ + { + "index": 0, + "delta": {}, + "finish_reason": "stop", + } + ], + "lisa_guardrail_triggered": True, + } + yield f"data: {json.dumps(final_chunk)}\n\n" + yield "data: [DONE]\n\n" + + +def create_guardrail_json_response(guardrail_response: str, model_id: str, created: int = 0) -> JSONResponse: + """ + Create a JSON response for a guardrail violation. + + Parameters + ---------- + guardrail_response : str + The guardrail response text. + model_id : str + The model ID associated with the request. + created : int, optional + The creation timestamp, by default 0. + + Returns + ------- + JSONResponse + A properly formatted JSON response for the guardrail violation. + """ + response_data = { + "id": "guardrail-response", + "object": "chat.completion", + "created": created, + "model": model_id, + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": guardrail_response}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}, + "lisa_guardrail_triggered": True, + } + return JSONResponse(response_data, status_code=200) diff --git a/lib/serve/serveApplicationConstruct.ts b/lib/serve/serveApplicationConstruct.ts index 6696eb9b5..c7f1b40e8 100644 --- a/lib/serve/serveApplicationConstruct.ts +++ b/lib/serve/serveApplicationConstruct.ts @@ -227,12 +227,19 @@ export class LisaServeApplicationConstruct extends Construct { this.modelsPs.grantRead(serveRole); } + // Get guardrails table name from parameter store + const guardrailsTableName = StringParameter.valueForStringParameter( + scope, + `${config.deploymentPrefix}/guardrailsTableName` + ); + // Add parameter as container environment variable for both RestAPI and RagAPI const container = restApi.apiCluster.containers[ECSTasks.REST]; if (container) { container.addEnvironment('LITELLM_DB_INFO_PS_NAME', litellmDbConnectionInfoPs.parameterName); container.addEnvironment('REGISTERED_MODELS_PS_NAME', this.modelsPs.parameterName); container.addEnvironment('LITELLM_DB_INFO_PS_NAME', litellmDbConnectionInfoPs.parameterName); + container.addEnvironment('GUARDRAILS_TABLE_NAME', guardrailsTableName); } restApi.node.addDependency(this.modelsPs); restApi.node.addDependency(litellmDbConnectionInfoPs); @@ -269,13 +276,36 @@ export class LisaServeApplicationConstruct extends Construct { ] }); + // Grant DynamoDB permissions for guardrails table + const guardrails_permissions = new Policy(scope, 'GuardrailsTablePerms', { + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'dynamodb:Query', + 'dynamodb:GetItem', + ], + resources: [ + `arn:${config.partition}:dynamodb:${config.region}:${config.accountNumber}:table/${guardrailsTableName}/*`, + ], + }), + ] + }); + // Grant SSM parameter read access and attach invocation permissions - if (serveRole) { - this.modelsPs.grantRead(serveRole); - litellmDbConnectionInfoPs.grantRead(serveRole); - serveRole.attachInlinePolicy(invocation_permissions); + const restRole = restApi.apiCluster.taskRoles[ECSTasks.REST]; + if (restRole) { + this.modelsPs.grantRead(restRole); + litellmDbConnectionInfoPs.grantRead(restRole); + restRole.attachInlinePolicy(invocation_permissions); + restRole.attachInlinePolicy(guardrails_permissions); + if (serveRole) { + this.modelsPs.grantRead(serveRole); + litellmDbConnectionInfoPs.grantRead(serveRole); + serveRole.attachInlinePolicy(invocation_permissions); + } } - } + }; getIAMAuthLambda (scope: Stack, config: Config, secret: ISecret, user: string, vpc: Vpc, securityGroups: ISecurityGroup[]): IFunction { // Create the IAM role for updating the database to allow IAM authentication @@ -397,4 +427,5 @@ export class LisaServeApplicationConstruct extends Construct { return { managementKeySecretName }; } + } diff --git a/lib/user-interface/react/src/components/chatbot/hooks/chat.hooks.tsx b/lib/user-interface/react/src/components/chatbot/hooks/chat.hooks.tsx index 2c43d3681..3926462f6 100644 --- a/lib/user-interface/react/src/components/chatbot/hooks/chat.hooks.tsx +++ b/lib/user-interface/react/src/components/chatbot/hooks/chat.hooks.tsx @@ -22,7 +22,7 @@ import { LisaChatSession, MessageTypes, ModelFeatures, } from '@/components/types'; -import { RESTAPI_URI, RESTAPI_VERSION } from '@/components/utils'; +import { RESTAPI_URI, RESTAPI_VERSION, markLastUserMessageAsGuardrailTriggered } from '@/components/utils'; import { IModel } from '@/shared/model/model-management.model'; import { GenerateLLMRequestParams, IChatConfiguration } from '@/shared/model/chat.configurations.model'; import { ChatMemory } from '@/shared/util/chat-memory'; @@ -145,8 +145,10 @@ export const useChatGeneration = ({ const llmClient = createOpenAiClient(chatConfiguration.sessionConfiguration.streaming); // Convert chat history to messages format - // Always concatenate session history with new messages - const messagesToProcess = session.history.concat(params.message); + // Filter out guardrail-triggered messages when sending to model + const filteredHistory = session.history.filter((msg) => !msg.guardrailTriggered); + // Always concatenate filtered session history with new messages + const messagesToProcess = filteredHistory.concat(params.message); let messages = messagesToProcess.map((msg) => { const baseMessage: any = { @@ -199,6 +201,8 @@ export const useChatGeneration = ({ const resp: string[] = []; const toolCallsAccumulator: { [index: number]: any } = {}; + let guardrailTriggered = false; + for await (const chunk of stream) { // Check if stop was requested if (stopRequested.current) { @@ -208,6 +212,13 @@ export const useChatGeneration = ({ const content = chunk.content as string; + // Check if this chunk indicates a guardrail was triggered + const isGuardrailTriggered = (chunk as any).id === 'guardrail-response'; + + if (isGuardrailTriggered) { + guardrailTriggered = true; + } + // Get tool calls from LangChain streaming chunks let tool_calls: any[] = []; @@ -355,17 +366,25 @@ export const useChatGeneration = ({ setSession((prev) => { const lastMessage = prev.history[prev.history.length - 1]; if (lastMessage?.type === MessageTypes.AI) { + let updatedHistory = [...prev.history.slice(0, -1), + new LisaChatMessage({ + ...lastMessage, + usage: { + ...lastMessage.usage, + responseTime: parseFloat(responseTime.toFixed(2)) + }, + guardrailTriggered: guardrailTriggered + }) + ]; + + // If guardrail was triggered, also mark the user message + if (guardrailTriggered) { + updatedHistory = markLastUserMessageAsGuardrailTriggered(updatedHistory); + } + return { ...prev, - history: [...prev.history.slice(0, -1), - new LisaChatMessage({ - ...lastMessage, - usage: { - ...lastMessage.usage, - responseTime: parseFloat(responseTime.toFixed(2)) - } - }) - ], + history: updatedHistory, }; } return prev; @@ -385,23 +404,40 @@ export const useChatGeneration = ({ const content = response.content as string; const usage = response.response_metadata.tokenUsage; + // Check if guardrail was triggered + const isGuardrailTriggered = (response as any)?.id === 'guardrail-response'; + // Calculate response time const responseTime = (performance.now() - startTime) / 1000; await memory.saveContext({ input: params.input }, { output: content }); - setSession((prev) => ({ - ...prev, - history: [...prev.history, new LisaChatMessage({ - type: 'ai', - content, - metadata, - toolCalls: [...(response.tool_calls ?? [])], - usage: { - ...usage, - responseTime: parseFloat(responseTime.toFixed(2)) - } - })], - })); + + // Create the AI message + const aiMessage = new LisaChatMessage({ + type: 'ai', + content, + metadata, + toolCalls: [...(response.tool_calls ?? [])], + usage: { + ...usage, + responseTime: parseFloat(responseTime.toFixed(2)) + }, + guardrailTriggered: isGuardrailTriggered + }); + + setSession((prev) => { + let updatedHistory = [...prev.history, aiMessage]; + + // If guardrail was triggered, also mark the user message + if (isGuardrailTriggered) { + updatedHistory = markLastUserMessageAsGuardrailTriggered(updatedHistory); + } + + return { + ...prev, + history: updatedHistory, + }; + }); } } } catch (error) { diff --git a/lib/user-interface/react/src/components/model-management/create-model/AutoScalingConfig.tsx b/lib/user-interface/react/src/components/model-management/create-model/AutoScalingConfig.tsx index b5327ec01..ab34da769 100644 --- a/lib/user-interface/react/src/components/model-management/create-model/AutoScalingConfig.tsx +++ b/lib/user-interface/react/src/components/model-management/create-model/AutoScalingConfig.tsx @@ -32,84 +32,86 @@ export function AutoScalingConfig (props: AutoScalingConfigProps) : ReactElement Auto Scaling Capacity}> - + + + props.touchFields(['autoScalingConfig.blockDeviceVolumeSize'])} disabled={props.isEdit} onChange={({ detail }) => { props.setFields({ 'autoScalingConfig.blockDeviceVolumeSize': Number(detail.value) }); }}/> GBs - - + + props.touchFields(['autoScalingConfig.minCapacity'])} onChange={({ detail }) => { props.setFields({ 'autoScalingConfig.minCapacity': Number(detail.value) }); }}/> instances - - + + props.touchFields(['autoScalingConfig.maxCapacity'])} onChange={({ detail }) => { props.setFields({ 'autoScalingConfig.maxCapacity': Number(detail.value) }); }}/> instances - - + + props.touchFields(['autoScalingConfig.desiredCapacity'])} onChange={({ detail }) => { props.setFields({ 'autoScalingConfig.desiredCapacity': detail.value.trim().length > 0 ? Number(detail.value) : undefined }); }}/> instances - - + + props.touchFields(['autoScalingConfig.cooldown'])} onChange={({ detail }) => { props.setFields({ 'autoScalingConfig.cooldown': Number(detail.value) }); }}/> seconds - - + + props.touchFields(['autoScalingConfig.defaultInstanceWarmup'])} onChange={({ detail }) => { props.setFields({ 'autoScalingConfig.defaultInstanceWarmup': Number(detail.value) }); }}/> seconds - + Metric Config}> - props.touchFields(['autoScalingConfig.metricConfig.albMetricName'])} disabled={props.isEdit} onChange={({ detail }) => { - props.setFields({ 'autoScalingConfig.metricConfig.albMetricName': detail.value }); - }}/> + props.touchFields(['autoScalingConfig.metricConfig.albMetricName'])} disabled={props.isEdit} onChange={({ detail }) => { + props.setFields({ 'autoScalingConfig.metricConfig.albMetricName': detail.value }); + }}/> - props.touchFields(['autoScalingConfig.metricConfig.targetValue'])} disabled={props.isEdit} onChange={({ detail }) => { - props.setFields({ 'autoScalingConfig.metricConfig.targetValue': Number(detail.value) }); - }}/> + props.touchFields(['autoScalingConfig.metricConfig.targetValue'])} disabled={props.isEdit} onChange={({ detail }) => { + props.setFields({ 'autoScalingConfig.metricConfig.targetValue': Number(detail.value) }); + }}/> - - props.touchFields(['autoScalingConfig.metricConfig.duration'])} disabled={props.isEdit} onChange={({ detail }) => { - props.setFields({ 'autoScalingConfig.metricConfig.duration': Number(detail.value) }); - }}/> - seconds - + + props.touchFields(['autoScalingConfig.metricConfig.duration'])} disabled={props.isEdit} onChange={({ detail }) => { + props.setFields({ 'autoScalingConfig.metricConfig.duration': Number(detail.value) }); + }}/> + seconds + - - props.touchFields(['autoScalingConfig.metricConfig.estimatedInstanceWarmup'])} disabled={props.isEdit} onChange={({ detail }) => { - props.setFields({ 'autoScalingConfig.metricConfig.estimatedInstanceWarmup': Number(detail.value) }); - }}/> - seconds - + + props.touchFields(['autoScalingConfig.metricConfig.estimatedInstanceWarmup'])} disabled={props.isEdit} onChange={({ detail }) => { + props.setFields({ 'autoScalingConfig.metricConfig.estimatedInstanceWarmup': Number(detail.value) }); + }}/> + seconds + diff --git a/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx b/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx index 14e11839e..d0319892f 100644 --- a/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx +++ b/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx @@ -38,123 +38,125 @@ export function BaseModelConfig (props: FormProps & BaseModelConf return ( - props.touchFields(['modelId'])} onChange={({ detail }) => { - props.setFields({ 'modelId': detail.value }); - }} disabled={props.isEdit} placeholder='mistral-vllm'/> + props.touchFields(['modelId'])} onChange={({ detail }) => { + props.setFields({ 'modelId': detail.value }); + }} disabled={props.isEdit} placeholder='mistral-vllm'/> - props.touchFields(['modelName'])} onChange={({ detail }) => { - props.setFields({ 'modelName': detail.value }); - }} disabled={props.isEdit} placeholder='mistralai/Mistral-7B-Instruct-v0.2'/> + constraintText='The full model name is the repository path, or the third party model provider path. The path format typically will be: {ProviderPath}/{ProviderModelName}. Users do not see this value in the chat assistant user interface.'> + + props.touchFields(['modelName'])} onChange={({ detail }) => { + props.setFields({ 'modelName': detail.value }); + }} disabled={props.isEdit} placeholder='mistralai/Mistral-7B-Instruct-v0.2'/> + Model Description (Optional)} errorText={props.formErrors?.modelDescription}> - Model Description (optional)} errorText={props.formErrors?.modelDescription}> - props.touchFields(['modelDescription'])} onChange={({ detail }) => { - props.setFields({ 'modelDescription': detail.value }); - }} placeholder='Brief description of the model and its capabilities'/> + props.touchFields(['modelDescription'])} onChange={({ detail }) => { + props.setFields({ 'modelDescription': detail.value }); + }} placeholder='Brief description of the model and its capabilities'/> + {!props.item.lisaHostedModel && <>API Key (Optional)} errorText={props.formErrors?.apiKey}> - {!props.item.lisaHostedModel && API Key (optional)} errorText={props.formErrors?.apiKey}> - props.touchFields(['apiKey'])} onChange={({ detail }) => { - props.setFields({ 'apiKey': detail.value }); - }} disabled={props.isEdit}/> - } - Model URL (optional)} errorText={props.formErrors?.modelUrl}> - props.touchFields(['modelUrl'])} onChange={({ detail }) => { - props.setFields({ 'modelUrl': detail.value }); - }} disabled={props.isEdit}/> + props.touchFields(['apiKey'])} onChange={({ detail }) => { + props.setFields({ 'apiKey': detail.value }); + }} disabled={props.isEdit}/>} + Model URL (Optional)} errorText={props.formErrors?.modelUrl}> + props.touchFields(['modelUrl'])} onChange={({ detail }) => { + props.setFields({ 'modelUrl': detail.value }); + }} disabled={props.isEdit}/> - { + const fields = { + 'modelType': detail.selectedOption.value, + }; - // turn off streaming for embedded models - if (fields.modelType === ModelType.embedding || fields.modelType === ModelType.imagegen) { - fields['streaming'] = false; - } + // turn off streaming for embedded models + if (fields.modelType === ModelType.embedding || fields.modelType === ModelType.imagegen) { + fields['streaming'] = false; + } - // turn off summarization and image input for embedded and imagegen models - if ((fields.modelType === ModelType.embedding || fields.modelType === ModelType.imagegen)) { - fields['features'] = props.item.features.filter((feature) => feature.name !== ModelFeatures.SUMMARIZATION && feature.name !== ModelFeatures.IMAGE_INPUT && feature.name !== ModelFeatures.TOOL_CALLS); - } + // turn off summarization and image input for embedded and imagegen models + if ((fields.modelType === ModelType.embedding || fields.modelType === ModelType.imagegen)) { + fields['features'] = props.item.features.filter((feature) => feature.name !== ModelFeatures.SUMMARIZATION && feature.name !== ModelFeatures.IMAGE_INPUT && feature.name !== ModelFeatures.TOOL_CALLS); + } - props.setFields(fields); - }} - onBlur={() => props.touchFields(['modelType'])} - options={[ - { label: 'TEXTGEN', value: ModelType.textgen }, - { label: 'IMAGEGEN', value: ModelType.imagegen }, - { label: 'EMBEDDING', value: ModelType.embedding }, - ]} - disabled={props.isEdit} - /> - + props.setFields(fields); + }} + onBlur={() => props.touchFields(['modelType'])} + options={[ + { label: 'TEXTGEN', value: ModelType.textgen }, + { label: 'IMAGEGEN', value: ModelType.imagegen }, + { label: 'EMBEDDING', value: ModelType.embedding }, + ]} + disabled={props.isEdit} + /> {props.item.lisaHostedModel && ( <> - ({value: instance}))} + selectedOption={{value: props.item.instanceType}} + loadingText='Loading instances' + disabled={props.isEdit} + onBlur={() => props.touchFields(['instanceType'])} + onChange={({ detail }) => { + props.setFields({ 'instanceType': detail.selectedOption.value }); + }} + filteringType='auto' + statusType={ isLoadingInstances ? 'loading' : 'finished'} + virtualScroll + /> - props.touchFields(['inferenceContainer'])} + onChange={({ detail }) => + props.setFields({ + 'inferenceContainer': detail.selectedOption.value, + }) + } + options={[ + { label: 'TGI', value: InferenceContainer.TGI }, + { label: 'TEI', value: InferenceContainer.TEI }, + { label: 'VLLM', value: InferenceContainer.VLLM }, + ]} + disabled={props.isEdit} + /> )} - + + + props.setFields({'streaming': detail.checked}) @@ -163,8 +165,10 @@ export function BaseModelConfig (props: FormProps & BaseModelConf disabled={isEmbeddingModel || isImageModel} checked={props.item.streaming} /> - - + + + + { if (detail.checked && props.item.features.find((feature) => feature.name === ModelFeatures.TOOL_CALLS) === undefined) { @@ -177,8 +181,10 @@ export function BaseModelConfig (props: FormProps & BaseModelConf onBlur={() => props.touchFields(['features'])} checked={props.item.features.find((feature) => feature.name === ModelFeatures.TOOL_CALLS) !== undefined} /> - - + + + + { if (detail.checked && props.item.features.find((feature) => feature.name === ModelFeatures.IMAGE_INPUT) === undefined) { @@ -191,9 +197,11 @@ export function BaseModelConfig (props: FormProps & BaseModelConf onBlur={() => props.touchFields(['features'])} checked={props.item.features.find((feature) => feature.name === ModelFeatures.IMAGE_INPUT) !== undefined} /> - - feature.name === ModelFeatures.SUMMARIZATION) !== undefined ? 'Ensure model context is large enough to support these requests.' : ''}> + + + feature.name === ModelFeatures.SUMMARIZATION) !== undefined ? 'Ensure model context is large enough to support these requests.' : ''}> + { if (detail.checked && props.item.features.find((feature) => feature.name === ModelFeatures.SUMMARIZATION) === undefined) { @@ -206,19 +214,19 @@ export function BaseModelConfig (props: FormProps & BaseModelConf onBlur={() => props.touchFields(['features'])} checked={props.item.features.find((feature) => feature.name === ModelFeatures.SUMMARIZATION) !== undefined} /> - + - - feature.name === ModelFeatures.SUMMARIZATION) !== undefined ? props.item.features.filter((feature) => feature.name === 'summarization')[0].overview : ''} inputMode='text' onBlur={() => props.touchFields(['features'])} onChange={({ detail }) => { - props.setFields({ 'features': [...props.item.features.filter((feature) => feature.name !== ModelFeatures.SUMMARIZATION), {name: ModelFeatures.SUMMARIZATION, overview: detail.value}] }); - }} disabled={!props.item.features.find((feature) => feature.name === ModelFeatures.SUMMARIZATION)} placeholder='Optional overview of Summarization for Model'/> + Summarization Capabilities (Optional)} errorText={props.formErrors?.summarizationCapabilities}> + feature.name === ModelFeatures.SUMMARIZATION) !== undefined ? props.item.features.filter((feature) => feature.name === 'summarization')[0].overview : ''} inputMode='text' onBlur={() => props.touchFields(['features'])} onChange={({ detail }) => { + props.setFields({ 'features': [...props.item.features.filter((feature) => feature.name !== ModelFeatures.SUMMARIZATION), {name: ModelFeatures.SUMMARIZATION, overview: detail.value}] }); + }} disabled={!props.item.features.find((feature) => feature.name === ModelFeatures.SUMMARIZATION)} placeholder='Overview of Summarization for Model'/> props.setFields({ 'allowedGroups': values })} - description='Restrict model access to specific groups. Leave empty to allow access to all users.' + constraintText='Restrict model access to specific groups. Leave empty to allow access to all users.' /> ); diff --git a/lib/user-interface/react/src/components/model-management/create-model/ContainerConfig.tsx b/lib/user-interface/react/src/components/model-management/create-model/ContainerConfig.tsx index 3b20905a7..e8c86449c 100644 --- a/lib/user-interface/react/src/components/model-management/create-model/ContainerConfig.tsx +++ b/lib/user-interface/react/src/components/model-management/create-model/ContainerConfig.tsx @@ -38,43 +38,43 @@ export function ContainerConfig (props: ContainerConfigProps) : ReactElement { > - - props.touchFields(['containerConfig.sharedMemorySize'])} - onChange={({ detail }) => { - props.setFields({ 'containerConfig.sharedMemorySize': Number(detail.value) }); - }} - /> - MiB - - + props.touchFields(['containerConfig.image.baseImage'])} + value={props.item.sharedMemorySize.toString()} + type='number' + inputMode='numeric' + onBlur={() => props.touchFields(['containerConfig.sharedMemorySize'])} onChange={({ detail }) => { - props.setFields({ 'containerConfig.image.baseImage': detail.value }); + props.setFields({ 'containerConfig.sharedMemorySize': Number(detail.value) }); }} /> + MiB + + + props.touchFields(['containerConfig.image.baseImage'])} + onChange={({ detail }) => { + props.setFields({ 'containerConfig.image.baseImage': detail.value }); + }} + /> - props.touchFields(['containerConfig.image.type'])} + onChange={({ detail }) => { + props.setFields({ 'containerConfig.image.type': detail.selectedOption.value }); + }} + options={[ + { label: 'asset', value: EcsSourceType.ASSET, description: 'Base container image used to build model hosting image, e.g. \'vllm/vllm-openai\'' }, + { label: 'ecr', value: EcsSourceType.ECR, description: 'Prebuilt ECR image url used when deploying to ECS' }, + ]} + /> - - {props.item.healthCheckConfig.command.map((item, index) => - - props.touchFields(['containerConfig.healthCheckConfig.command'])} onChange={({ detail }) => { - props.setFields({ 'containerConfig.healthCheckConfig.command' : props.item.healthCheckConfig.command.map((item, i) => i === index ? detail.value : item) }); - }}/> - - - )} - - + + {props.item.healthCheckConfig.command.map((item, index) => + + props.touchFields(['containerConfig.healthCheckConfig.command'])} onChange={({ detail }) => { + props.setFields({ 'containerConfig.healthCheckConfig.command' : props.item.healthCheckConfig.command.map((item, i) => i === index ? detail.value : item) }); + }}/> + + + )} + + - - props.touchFields(['containerConfig.healthCheckConfig.interval'])} onChange={({ detail }) => { - props.setFields({ 'containerConfig.healthCheckConfig.interval': Number(detail.value) }); - }}/> - seconds - + + props.touchFields(['containerConfig.healthCheckConfig.interval'])} onChange={({ detail }) => { + props.setFields({ 'containerConfig.healthCheckConfig.interval': Number(detail.value) }); + }}/> + seconds + - - props.touchFields(['containerConfig.healthCheckConfig.startPeriod'])} onChange={({ detail }) => { - props.setFields({ 'containerConfig.healthCheckConfig.startPeriod': Number(detail.value) }); - }}/> - seconds - + + props.touchFields(['containerConfig.healthCheckConfig.startPeriod'])} onChange={({ detail }) => { + props.setFields({ 'containerConfig.healthCheckConfig.startPeriod': Number(detail.value) }); + }}/> + seconds + - - props.touchFields(['containerConfig.healthCheckConfig.timeout'])} onChange={({ detail }) => { - props.setFields({ 'containerConfig.healthCheckConfig.timeout': Number(detail.value) }); - }}/> - seconds - - - props.touchFields(['containerConfig.healthCheckConfig.retries'])} onChange={({ detail }) => { - props.setFields({ 'containerConfig.healthCheckConfig.retries': Number(detail.value) }); + + props.touchFields(['containerConfig.healthCheckConfig.timeout'])} onChange={({ detail }) => { + props.setFields({ 'containerConfig.healthCheckConfig.timeout': Number(detail.value) }); }}/> + seconds + + + props.touchFields(['containerConfig.healthCheckConfig.retries'])} onChange={({ detail }) => { + props.setFields({ 'containerConfig.healthCheckConfig.retries': Number(detail.value) }); + }}/> value !== undefined ); } + + // Handle guardrailsConfig if present + if (updateFields.guardrailsConfig !== undefined) { + // Send the complete current guardrails config from state.form, not just the diff + // This ensures all guardrails are preserved unless explicitly marked for deletion + updateRequest.guardrailsConfig = state.form.guardrailsConfig; + } + updateModelMutation(updateRequest); } } @@ -353,6 +363,15 @@ export function CreateModelModal (props: CreateModelModalProps) : ReactElement { onEdit: state.form.lisaHostedModel, forExternalModel: false }, + { + title: 'Guardrails Configuration', + description: 'Configure guardrails for your model (optional).', + content: ( + + ), + onEdit: true, + forExternalModel: true + }, { title: `Review and ${props.isEdit ? 'Update' : 'Create'}`, description: `Review configuration ${props.isEdit ? 'changes' : ''} prior to submitting.`, diff --git a/lib/user-interface/react/src/components/model-management/create-model/GuardrailsConfig.tsx b/lib/user-interface/react/src/components/model-management/create-model/GuardrailsConfig.tsx new file mode 100644 index 000000000..74a2bd964 --- /dev/null +++ b/lib/user-interface/react/src/components/model-management/create-model/GuardrailsConfig.tsx @@ -0,0 +1,283 @@ +/** + 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 React, { ReactElement, useState } from 'react'; +import { FormProps } from '../../../shared/form/form-props'; +import { IGuardrailsConfig, GuardrailMode } from '../../../shared/model/model-management.model'; +import { + Button, + Container, + FormField, + Grid, + Header, + Icon, + Input, + Select, + SpaceBetween, + Textarea, + TokenGroup +} from '@cloudscape-design/components'; + +type GuardrailsConfigProps = FormProps & { + isEdit: boolean; +}; + +export function GuardrailsConfig (props: GuardrailsConfigProps): ReactElement { + const guardrails = props.item || {}; + const guardrailEntries = Object.entries(guardrails); + const [groupInputValues, setGroupInputValues] = useState>({}); + + const addGuardrail = () => { + const newKey = `guardrail-${Date.now()}`; + const newGuardrails = { + ...guardrails, + [newKey]: { + guardrailName: '', + guardrailIdentifier: '', + guardrailVersion: 'DRAFT', + mode: GuardrailMode.PRE_CALL, + description: '', + allowedGroups: [], + } + }; + props.setFields({ 'guardrailsConfig': newGuardrails }); + }; + + const removeGuardrail = (key: string) => { + if (props.isEdit) { + // Mark for deletion instead of removing + const updatedGuardrails = { + ...guardrails, + [key]: { + ...guardrails[key], + markedForDeletion: true + } + }; + props.setFields({ 'guardrailsConfig': updatedGuardrails }); + } else { + // Remove completely for new models + const remainingGuardrails = Object.fromEntries( + Object.entries(guardrails).filter(([k]) => k !== key) + ); + + props.setFields({ 'guardrailsConfig': remainingGuardrails }); + } + props.touchFields(['guardrailsConfig']); + }; + + const updateGuardrail = (key: string, field: string, value: any) => { + const updatedGuardrails = { + ...guardrails, + [key]: { + ...guardrails[key], + [field]: value + } + }; + props.setFields({ 'guardrailsConfig': updatedGuardrails }); + }; + + const modeOptions = [ + { label: 'Pre Call', value: GuardrailMode.PRE_CALL, description: 'Execute guardrail before LLM call' }, + { label: 'During Call', value: GuardrailMode.DURING_CALL, description: 'Execute guardrail during LLM call' }, + { label: 'Post Call', value: GuardrailMode.POST_CALL, description: 'Execute guardrail after LLM call' } + ]; + + return ( + + + Add Guardrail + + } + > + Guardrails Configuration + + } + > + {guardrailEntries.length === 0 ? ( +
+ No guardrails configured. Click "Add Guardrail" to create one. +
+ ) : ( + + {guardrailEntries.map(([key, guardrail]) => { + // Skip rendering guardrails marked for deletion + if (guardrail.markedForDeletion) { + return null; + } + + return ( + +
+ {guardrail.guardrailName || 'New Guardrail'} +
+
+ +
+ + } + > + + + updateGuardrail(key, 'guardrailName', detail.value)} + onBlur={() => props.touchFields([`guardrailsConfig.guardrails.${key}.guardrailName`])} + placeholder='Enter guardrail name' + /> + + updateGuardrail(key, 'guardrailIdentifier', detail.value)} + onBlur={() => props.touchFields([`guardrailsConfig.guardrails.${key}.guardrailIdentifier`])} + placeholder='Enter guardrail identifier (ARN or ID)' + /> + + + updateGuardrail(key, 'guardrailVersion', detail.value)} + onBlur={() => props.touchFields([`guardrailsConfig.guardrails.${key}.guardrailVersion`])} + placeholder='Enter version (e.g., DRAFT, 1, 2)' + /> + + +