Skip to content

Commit 81e1b86

Browse files
committed
Merge branch 'fix/validate-lambda-builds-mac-compatibility' into 'develop'
Fix validate_lambda_builds Mac compatibility See merge request genaiic-reusable-assets/engagement-artifacts/genaiic-idp-accelerator!281
2 parents 7a2c708 + 7cd7958 commit 81e1b86

3 files changed

Lines changed: 101 additions & 29 deletions

File tree

docs/deployment.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ You need to have the following packages installed on your computer:
2626
3. [sam (AWS SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html)
2727
4. python 3.11 or later
2828
5. A local Docker daemon
29-
6. Python packages for publish.py: `pip install boto3 typer rich botocore setuptools`
29+
6. Python packages for publish.py: `pip install boto3 rich PyYAML botocore setuptools`
3030

3131
For guidance on setting up a development environment, see: [Development Environment Setup Guide on Linux](./setup-development-env-linux.md) or [Development Environment Setup Guide on MacOS](./setup-development-env-macos.md)
3232

publish.py

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import hashlib
1414
import json
1515
import os
16-
import platform
1716
import shutil
1817
import subprocess
1918
import sys
@@ -23,6 +22,7 @@
2322
from urllib.parse import quote
2423

2524
import boto3
25+
import yaml
2626
from botocore.exceptions import ClientError
2727
from rich.console import Console
2828
from rich.progress import (
@@ -53,10 +53,50 @@ def __init__(self, verbose=False):
5353
self.public = False
5454
self.main_template = "idp-main.yaml"
5555
self.use_container_flag = ""
56-
self.stat_cmd = None
56+
5757
self.s3_client = None
5858
self.cf_client = None
5959
self._is_lib_changed = False
60+
self.skip_validation = False
61+
62+
def clean_checksums(self):
63+
"""Delete all .checksum files in main, patterns, options, and lib directories"""
64+
self.console.print("[yellow]🧹 Cleaning all .checksum files...[/yellow]")
65+
66+
checksum_paths = [
67+
".checksum", # main
68+
"lib/.checksum", # lib
69+
]
70+
71+
# Add patterns checksum files
72+
patterns_dir = "patterns"
73+
if os.path.exists(patterns_dir):
74+
for item in os.listdir(patterns_dir):
75+
pattern_path = os.path.join(patterns_dir, item)
76+
if os.path.isdir(pattern_path):
77+
checksum_paths.append(f"{pattern_path}/.checksum")
78+
79+
# Add options checksum files
80+
options_dir = "options"
81+
if os.path.exists(options_dir):
82+
for item in os.listdir(options_dir):
83+
option_path = os.path.join(options_dir, item)
84+
if os.path.isdir(option_path):
85+
checksum_paths.append(f"{option_path}/.checksum")
86+
87+
deleted_count = 0
88+
for checksum_path in checksum_paths:
89+
if os.path.exists(checksum_path):
90+
os.remove(checksum_path)
91+
self.console.print(f"[green] ✓ Deleted {checksum_path}[/green]")
92+
deleted_count += 1
93+
94+
if deleted_count == 0:
95+
self.console.print("[dim] No .checksum files found to delete[/dim]")
96+
else:
97+
self.console.print(
98+
f"[green]✅ Deleted {deleted_count} .checksum files - full rebuild will be triggered[/green]"
99+
)
60100

61101
def log_verbose(self, message, style="dim"):
62102
"""Log verbose messages if verbose mode is enabled"""
@@ -119,7 +159,7 @@ def print_usage(self):
119159
"""Print usage information with Rich formatting"""
120160
self.console.print("\n[bold cyan]Usage:[/bold cyan]")
121161
self.console.print(
122-
" python3 publish.py <cfn_bucket_basename> <cfn_prefix> <region> [public] [--max-workers N] [--verbose]"
162+
" python3 publish.py <cfn_bucket_basename> <cfn_prefix> <region> [public] [--max-workers N] [--verbose] [--no-validate] [--clean-build]"
123163
)
124164

125165
self.console.print("\n[bold cyan]Parameters:[/bold cyan]")
@@ -140,6 +180,12 @@ def print_usage(self):
140180
self.console.print(
141181
" [yellow][--verbose, -v][/yellow]: Optional. Enable verbose output for debugging"
142182
)
183+
self.console.print(
184+
" [yellow][--no-validate][/yellow]: Optional. Skip CloudFormation template validation"
185+
)
186+
self.console.print(
187+
" [yellow][--clean-build][/yellow]: Optional. Delete all .checksum files to force full rebuild"
188+
)
143189

144190
def check_parameters(self, args):
145191
"""Check and validate input parameters"""
@@ -197,6 +243,13 @@ def check_parameters(self, args):
197243
elif arg in ["--verbose", "-v"]:
198244
# Verbose flag is already handled by Typer, just acknowledge it here
199245
pass
246+
elif arg == "--no-validate":
247+
self.skip_validation = True
248+
self.console.print(
249+
"[yellow]CloudFormation template validation will be skipped[/yellow]"
250+
)
251+
elif arg == "--clean-build":
252+
self.clean_checksums()
200253
else:
201254
self.console.print(
202255
f"[yellow]Warning: Unknown argument '{arg}' ignored[/yellow]"
@@ -228,12 +281,6 @@ def setup_environment(self):
228281
self.prefix_and_version = f"{self.prefix}/{self.version}"
229282
self.bucket = f"{self.bucket_basename}-{self.region}"
230283

231-
# Set platform-specific commands
232-
if platform.machine() == "x86_64":
233-
self.stat_cmd = "stat --format='%Y'"
234-
else:
235-
self.stat_cmd = "stat -f %m"
236-
237284
# Set UDOP model path based on region
238285
if self.region == "us-east-1":
239286
self.public_sample_udop_model = "s3://aws-ml-blog-us-east-1/artifacts/genai-idp/udop-finetuning/rvl-cdip/model.tar.gz"
@@ -528,7 +575,8 @@ def build_and_package_template(self, directory, force_rebuild=False):
528575
self._delete_checksum_file(directory)
529576
self.log_verbose(f"Exception in build_and_package_template: {e}")
530577
self.log_verbose(f"Traceback: {traceback.format_exc()}")
531-
return False
578+
self.console.print(f"[red]❌ Build failed for {directory}: {e}[/red]")
579+
sys.exit(1)
532580

533581
return True
534582

@@ -878,18 +926,25 @@ def _check_requirements_has_idp_common_pkg(self, func_dir):
878926

879927
return False, "No idp_common_pkg found in requirements.txt"
880928
except Exception as e:
881-
return False, f"Error reading requirements.txt: {e}"
929+
self.console.print(
930+
f"[red]❌ Error reading requirements.txt in {func_dir}: {e}[/red]"
931+
)
932+
sys.exit(1)
882933

883934
def _extract_function_name(self, dir_name, template_path):
884935
"""Extract CloudFormation function name from template by matching CodeUri."""
885936
try:
886-
import yaml
887-
888937
# Create a custom loader that ignores CloudFormation intrinsic functions
889938
class CFLoader(yaml.SafeLoader):
890939
pass
891940

892941
def construct_unknown(loader, node):
942+
if isinstance(node, yaml.ScalarNode):
943+
return loader.construct_scalar(node)
944+
elif isinstance(node, yaml.SequenceNode):
945+
return loader.construct_sequence(node)
946+
elif isinstance(node, yaml.MappingNode):
947+
return loader.construct_mapping(node)
893948
return None
894949

895950
# Add constructors for CloudFormation intrinsic functions
@@ -915,17 +970,21 @@ def construct_unknown(loader, node):
915970
for func in cf_functions:
916971
CFLoader.add_constructor(func, construct_unknown)
917972

918-
with open(template_path, "r") as f:
973+
with open(template_path, "r", encoding="utf-8") as f:
919974
template = yaml.load(f, Loader=CFLoader)
920975

976+
if not template or not isinstance(template, dict):
977+
raise Exception(f"Failed to parse YAML template: {template_path}")
978+
921979
resources = template.get("Resources", {})
922980
for resource_name, resource_config in resources.items():
923981
if (
924982
resource_config
983+
and isinstance(resource_config, dict)
925984
and resource_config.get("Type") == "AWS::Serverless::Function"
926985
):
927986
properties = resource_config.get("Properties", {})
928-
if properties:
987+
if properties and isinstance(properties, dict):
929988
code_uri = properties.get("CodeUri", "")
930989
if isinstance(code_uri, str):
931990
code_uri = code_uri.rstrip("/")
@@ -934,12 +993,15 @@ def construct_unknown(loader, node):
934993
)
935994
if code_dir == dir_name:
936995
return resource_name
937-
938-
return dir_name
996+
raise Exception(
997+
f"No CloudFormation function found for directory {dir_name} in template {template_path}"
998+
)
939999

9401000
except Exception as e:
941-
self.log_verbose(f"Error reading template {template_path}: {e}")
942-
return dir_name
1001+
self.console.print(
1002+
f"[red]❌ Error extracting function name for {dir_name} from {template_path}: {e}[/red]"
1003+
)
1004+
sys.exit(1)
9431005

9441006
def _validate_idp_common_in_build(self, template_dir, function_name, source_path):
9451007
"""Validate that idp_common package exists in the built Lambda function."""
@@ -1298,10 +1360,15 @@ def build_main_template(self, webui_zipfile, components_needing_rebuild):
12981360
)
12991361

13001362
# Validate the template
1301-
template_url = f"https://s3.{self.region}.amazonaws.com/{self.bucket}/{templates[0][0]}"
1302-
self.console.print(f"[cyan]Validating template: {template_url}[/cyan]")
1303-
self.cf_client.validate_template(TemplateURL=template_url)
1304-
self.console.print("[green]✅ Template validation passed[/green]")
1363+
if self.skip_validation:
1364+
self.console.print(
1365+
"[yellow]⚠️ Skipping CloudFormation template validation[/yellow]"
1366+
)
1367+
else:
1368+
template_url = f"https://s3.{self.region}.amazonaws.com/{self.bucket}/{templates[0][0]}"
1369+
self.console.print(f"[cyan]Validating template: {template_url}[/cyan]")
1370+
self.cf_client.validate_template(TemplateURL=template_url)
1371+
self.console.print("[green]✅ Template validation passed[/green]")
13051372

13061373
except ClientError as e:
13071374
# Delete checksum on template validation failure

publish.sh

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,19 @@ check_python_version() {
8888
check_and_install_packages() {
8989
print_info "Checking required Python packages..."
9090

91-
# List of required packages
92-
required_packages=("typer" "rich" "boto3")
91+
# List of required packages (import_name:package_name)
92+
declare -A required_packages=(
93+
["typer"]="typer"
94+
["rich"]="rich"
95+
["boto3"]="boto3"
96+
["yaml"]="PyYAML"
97+
)
9398
missing_packages=()
9499

95100
# Check each package
96-
for package in "${required_packages[@]}"; do
97-
if ! $PYTHON_CMD -c "import $package" >/dev/null 2>&1; then
98-
missing_packages+=("$package")
101+
for import_name in "${!required_packages[@]}"; do
102+
if ! $PYTHON_CMD -c "import $import_name" >/dev/null 2>&1; then
103+
missing_packages+=("${required_packages[$import_name]}")
99104
fi
100105
done
101106

0 commit comments

Comments
 (0)