Skip to content

Commit aeb0219

Browse files
committed
Modularise project structure and update documentation
1 parent 7f608bd commit aeb0219

File tree

26 files changed

+1210
-1842
lines changed

26 files changed

+1210
-1842
lines changed

README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,21 @@ packages/
2929
│ ├── resources/ # AWS resource definitions
3030
│ └── stacks/ # CDK stack definitions
3131
├── createIndexFunction/ # Lambda function for OpenSearch index management
32-
│ └── tests/ # Unit tests for createIndex function
32+
│ ├── app/ # Application code
33+
│ │ ├── config/ # Configuration and environment variables
34+
│ │ └── handler.py # Lambda handler
35+
│ └── tests/ # Unit tests
3336
├── slackBotFunction/ # Lambda function for Slack bot integration
34-
│ └── tests/ # Unit tests for slackBot function
37+
│ ├── app/ # Application code
38+
│ │ ├── config/ # Configuration and environment variables
39+
│ │ ├── slack/ # Slack-specific logic
40+
│ │ └── handler.py # Lambda handler
41+
│ └── tests/ # Unit tests
3542
└── syncKnowledgeBaseFunction/ # Lambda function for automatic knowledge base sync
36-
└── tests/ # Unit tests for syncKnowledgeBase function
43+
├── app/ # Application code
44+
│ ├── config/ # Configuration and environment variables
45+
│ └── handler.py # Lambda handler
46+
└── tests/ # Unit tests
3747
```
3848

3949
## Contributing
@@ -156,14 +166,15 @@ These are used to do common commands related to cdk
156166

157167
#### Linting and testing
158168

159-
- `lint` Runs lint for GitHub Actions and scripts.
169+
- `lint` Runs all linting checks
160170
- `lint-black` Runs black formatter on Python code.
161171
- `lint-flake8` Runs flake8 linter on Python code.
162172
- `lint-githubactions` Lints the repository's GitHub Actions workflows.
163173
- `lint-githubaction-scripts` Lints all shell scripts in `.github/scripts` using ShellCheck.
164-
- `test` Runs unit tests for Lambda functions.
165174
- `cfn-guard` Runs cfn-guard against CDK resources.
175+
- `git-secrets-docker-setup` Sets up git-secrets Docker container.
166176
- `pre-commit` Runs pre-commit hooks on all files.
177+
- `test` Runs unit tests for Lambda functions.
167178

168179
#### Compiling
169180

packages/cdk/constructs/LambdaFunction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class LambdaFunction extends Construct {
132132
memorySize: 256,
133133
timeout: Duration.seconds(50),
134134
architecture: Architecture.X86_64,
135-
handler: "app.handler",
135+
handler: "app.handler.handler",
136136
code: Code.fromAsset(props.packageBasePath),
137137
role,
138138
environment: {

packages/cdk/resources/Functions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class Functions extends Construct {
4444
stackName: props.stackName,
4545
functionName: `${props.stackName}-CreateIndexFunction`,
4646
packageBasePath: "packages/createIndexFunction",
47-
entryPoint: "app.py",
47+
entryPoint: "app.handler.py",
4848
logRetentionInDays: props.logRetentionInDays,
4949
logLevel: props.logLevel,
5050
environmentVariables: {"INDEX_NAME": props.collectionId},
@@ -56,7 +56,7 @@ export class Functions extends Construct {
5656
stackName: props.stackName,
5757
functionName: `${props.stackName}-SlackBotFunction`,
5858
packageBasePath: "packages/slackBotFunction",
59-
entryPoint: "app.py",
59+
entryPoint: "app.handler.py",
6060
logRetentionInDays: props.logRetentionInDays,
6161
logLevel: props.logLevel,
6262
additionalPolicies: [props.slackBotManagedPolicy],
@@ -82,7 +82,7 @@ export class Functions extends Construct {
8282
stackName: props.stackName,
8383
functionName: `${props.stackName}-SyncKnowledgeBaseFunction`,
8484
packageBasePath: "packages/syncKnowledgeBaseFunction",
85-
entryPoint: "app.py",
85+
entryPoint: "app.handler.py",
8686
logRetentionInDays: props.logRetentionInDays,
8787
logLevel: props.logLevel,
8888
environmentVariables: {

packages/createIndexFunction/app/__init__.py

Whitespace-only changes.

packages/createIndexFunction/app/config/__init__.py

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import os
2+
from aws_lambda_powertools import Logger
3+
4+
logger = Logger(service="createIndexFunction")
5+
6+
# Environment variables
7+
AWS_REGION = os.getenv("AWS_REGION", "eu-west-2")
8+
INDEX_NAME = os.environ.get("INDEX_NAME")

packages/createIndexFunction/app.py renamed to packages/createIndexFunction/app/handler.py

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,23 @@
11
import json
2-
import os
32
import time
4-
53
import boto3
64
from opensearchpy import OpenSearch, RequestsHttpConnection, AWSV4SignerAuth
75
from aws_lambda_powertools import Logger
86
from aws_lambda_powertools.utilities.typing import LambdaContext
7+
from app.config.config import AWS_REGION
98

109
logger = Logger(service="createIndexFunction")
1110

1211

1312
def get_opensearch_client(endpoint):
14-
"""
15-
Create an OpenSearch (AOSS) client using AWS credentials.
16-
Works for both AOSS and legacy OpenSearch domains by checking the endpoint.
17-
"""
13+
"""Create an OpenSearch (AOSS) client using AWS credentials."""
1814
service = "aoss" if "aoss" in endpoint else "es"
1915
logger.debug(f"Connecting to OpenSearch service: {service} at {endpoint}")
2016
return OpenSearch(
2117
hosts=[{"host": endpoint, "port": 443}],
2218
http_auth=AWSV4SignerAuth(
2319
boto3.Session().get_credentials(),
24-
os.getenv("AWS_REGION", "eu-west-2"),
20+
AWS_REGION,
2521
service,
2622
),
2723
use_ssl=True,
@@ -32,17 +28,12 @@ def get_opensearch_client(endpoint):
3228

3329

3430
def wait_for_index_aoss(opensearch_client, index_name, timeout=300, poll_interval=5):
35-
"""
36-
Wait until the index exists in OpenSearch Serverless (AOSS).
37-
AOSS does not support cluster health checks, so existence == ready.
38-
"""
31+
"""Wait until the index exists in OpenSearch Serverless (AOSS)."""
3932
logger.info(f"Waiting for index '{index_name}' to be available in AOSS...")
4033
start = time.time()
4134
while True:
4235
try:
43-
# Use .exists and then attempt to get mapping
4436
if opensearch_client.indices.exists(index=index_name):
45-
# Now check if mappings are available (index is queryable)
4637
mapping = opensearch_client.indices.get_mapping(index=index_name)
4738
if mapping and index_name in mapping:
4839
logger.info(f"Index '{index_name}' exists and mappings are ready.")
@@ -51,18 +42,14 @@ def wait_for_index_aoss(opensearch_client, index_name, timeout=300, poll_interva
5142
logger.info(f"Index '{index_name}' does not exist yet...")
5243
except Exception as exc:
5344
logger.info(f"Still waiting for index '{index_name}': {exc}")
54-
# Exit on timeout to avoid infinite loop during stack failures.
5545
if time.time() - start > timeout:
5646
logger.error(f"Timed out waiting for index '{index_name}' to be available.")
5747
return False
5848
time.sleep(poll_interval)
5949

6050

6151
def create_and_wait_for_index(client, index_name):
62-
"""
63-
Creates the index (if not present) and waits until it's ready for use.
64-
Idempotent: Does nothing if the index is already present.
65-
"""
52+
"""Creates the index (if not present) and waits until it's ready for use."""
6653
params = {
6754
"index": index_name,
6855
"body": {
@@ -98,40 +85,32 @@ def create_and_wait_for_index(client, index_name):
9885
}
9986

10087
try:
101-
# Only create if not present (safe for repeat runs/rollbacks)
10288
if not client.indices.exists(index=params["index"]):
10389
logger.info(f"Creating index {params['index']}")
10490
client.indices.create(index=params["index"], body=params["body"])
10591
logger.info(f"Index {params['index']} creation initiated.")
10692
else:
10793
logger.info(f"Index {params['index']} already exists")
10894

109-
# Wait until available for downstream resources
11095
if not wait_for_index_aoss(client, params["index"]):
11196
raise RuntimeError(f"Index {params['index']} failed to appear in time")
11297

11398
logger.info(f"Index {params['index']} is ready and active.")
11499
except Exception as e:
115100
logger.error(f"Error creating or waiting for index: {e}")
116-
raise e # Fail stack if this fails
101+
raise e
117102

118103

119104
def extract_parameters(event):
120-
"""
121-
Extract parameters from Lambda event, handling both:
122-
- CloudFormation custom resource invocations
123-
- Direct Lambda/test calls
124-
"""
105+
"""Extract parameters from Lambda event."""
125106
if "ResourceProperties" in event:
126-
# From CloudFormation custom resource
127107
properties = event["ResourceProperties"]
128108
return {
129109
"endpoint": properties.get("Endpoint"),
130110
"index_name": properties.get("IndexName"),
131111
"request_type": event.get("RequestType"),
132112
}
133113
else:
134-
# From direct Lambda invocation (e.g., manual test)
135114
return {
136115
"endpoint": event.get("Endpoint"),
137116
"index_name": event.get("IndexName"),
@@ -141,35 +120,27 @@ def extract_parameters(event):
141120

142121
@logger.inject_lambda_context
143122
def handler(event: dict, context: LambdaContext) -> dict:
144-
"""
145-
Entrypoint: create, update, or delete the OpenSearch index.
146-
Invoked via CloudFormation custom resource or manually.
147-
"""
123+
"""Entrypoint: create, update, or delete the OpenSearch index."""
148124
logger.info("Received event", extra={"event": event})
149125

150126
try:
151-
# CloudFormation custom resources may pass the actual event as a JSON string in "Payload"
152127
if "Payload" in event:
153128
event = json.loads(event["Payload"])
154129

155-
# Get parameters (handles both invocation types)
156130
params = extract_parameters(event)
157131
endpoint = params["endpoint"]
158132
index_name = params["index_name"]
159133
request_type = params["request_type"]
160134

161-
# Sanity check required parameters
162135
if not endpoint or not index_name or not request_type:
163136
raise ValueError("Missing required parameters: Endpoint, IndexName, or RequestType")
164137

165138
client = get_opensearch_client(endpoint)
166139

167140
if request_type in ["Create", "Update"]:
168-
# Idempotent: will not fail if index already exists
169141
create_and_wait_for_index(client, index_name)
170142
return {"PhysicalResourceId": f"index-{index_name}", "Status": "SUCCESS"}
171143
elif request_type == "Delete":
172-
# Clean up the index if it exists
173144
try:
174145
if client.indices.exists(index=index_name):
175146
client.indices.delete(index=index_name)

0 commit comments

Comments
 (0)