Skip to content

Commit 6480a85

Browse files
committed
feat: IDP CLI --from-code Flag for Local Development Deployment, and --no-rollback Flag for Stack Deployment Troubleshooting
1 parent 1ea86ad commit 6480a85

File tree

4 files changed

+205
-12
lines changed

4 files changed

+205
-12
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,17 @@ SPDX-License-Identifier: MIT-0
55

66
## [Unreleased]
77

8+
## [0.4.4]
9+
810
### Added
911

12+
- **IDP CLI --from-code Flag for Local Development Deployment**
13+
- Added `--from-code` flag to `idp-cli deploy` command enabling deployment directly from local source code
14+
- Automatically builds project using `publish.py` script with streaming output for real-time build progress
15+
- **IDP CLI --no-rollback Flag for Stack Deployment Troubleshooting**
16+
- Added `--no-rollback` flag to `idp-cli deploy` command to disable automatic rollback on CloudFormation stack creation failure
17+
- When enabled, failed stacks remain in `CREATE_FAILED` state instead of rolling back, allowing inspection of failed resources for troubleshooting
18+
1019
### Fixed
1120

1221
## [0.4.3]

idp_cli/idp_cli/cli.py

Lines changed: 153 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
"""
99

1010
import logging
11+
import os
12+
import subprocess
1113
import sys
1214
import time
1315
from typing import Optional
1416

17+
import boto3
1518
import click
1619
from rich.console import Console
1720
from rich.live import Live
@@ -31,6 +34,105 @@
3134

3235
console = Console()
3336

37+
38+
def _build_from_local_code(from_code_dir: str, region: str, stack_name: str) -> tuple:
39+
"""
40+
Build project from local code using publish.py
41+
42+
Args:
43+
from_code_dir: Path to project root directory
44+
region: AWS region
45+
stack_name: CloudFormation stack name (unused but kept for signature compatibility)
46+
47+
Returns:
48+
Tuple of (template_path, None) on success
49+
50+
Raises:
51+
SystemExit: On build failure
52+
"""
53+
# Verify publish.py exists
54+
publish_script = os.path.join(from_code_dir, "publish.py")
55+
if not os.path.isfile(publish_script):
56+
console.print(f"[red]✗ Error: publish.py not found in {from_code_dir}[/red]")
57+
console.print(
58+
"[yellow]Tip: --from-code should point to the project root directory[/yellow]"
59+
)
60+
sys.exit(1)
61+
62+
# Get AWS account ID
63+
try:
64+
sts = boto3.client("sts", region_name=region)
65+
account_id = sts.get_caller_identity()["Account"]
66+
except Exception as e:
67+
console.print(f"[red]✗ Error: Failed to get AWS account ID: {e}[/red]")
68+
sys.exit(1)
69+
70+
# Set parameters for publish.py
71+
cfn_bucket_basename = f"idp-accelerator-artifacts-{account_id}"
72+
cfn_prefix = "idp-cli"
73+
74+
console.print("[bold cyan]Building project from source...[/bold cyan]")
75+
console.print(f"[dim]Bucket: {cfn_bucket_basename}[/dim]")
76+
console.print(f"[dim]Prefix: {cfn_prefix}[/dim]")
77+
console.print(f"[dim]Region: {region}[/dim]")
78+
console.print()
79+
80+
# Build command
81+
cmd = [
82+
sys.executable, # Use same Python interpreter
83+
publish_script,
84+
cfn_bucket_basename,
85+
cfn_prefix,
86+
region,
87+
]
88+
89+
console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
90+
console.print()
91+
92+
# Run with streaming output
93+
try:
94+
process = subprocess.Popen(
95+
cmd,
96+
stdout=subprocess.PIPE,
97+
stderr=subprocess.STDOUT,
98+
text=True,
99+
bufsize=1,
100+
cwd=from_code_dir,
101+
)
102+
103+
# Stream output line by line
104+
for line in process.stdout:
105+
# Print each line immediately (preserve formatting from publish.py)
106+
print(line, end="")
107+
108+
process.wait()
109+
110+
if process.returncode != 0:
111+
console.print("[red]✗ Build failed. See output above for details.[/red]")
112+
sys.exit(1)
113+
114+
except Exception as e:
115+
console.print(f"[red]✗ Error running publish.py: {e}[/red]")
116+
sys.exit(1)
117+
118+
# Verify template was created
119+
template_path = os.path.join(from_code_dir, ".aws-sam", "idp-main.yaml")
120+
if not os.path.isfile(template_path):
121+
console.print(
122+
f"[red]✗ Error: Built template not found at {template_path}[/red]"
123+
)
124+
console.print(
125+
"[yellow]The build may have failed or the template was not generated.[/yellow]"
126+
)
127+
sys.exit(1)
128+
129+
console.print()
130+
console.print(f"[green]✓ Build complete. Using template: {template_path}[/green]")
131+
console.print()
132+
133+
return template_path, None
134+
135+
34136
# Region-specific template URLs
35137
TEMPLATE_URLS = {
36138
"us-west-2": "https://s3.us-west-2.amazonaws.com/aws-ml-blog-us-west-2/artifacts/genai-idp/idp-main.yaml",
@@ -40,7 +142,7 @@
40142

41143

42144
@click.group()
43-
@click.version_option(version="0.4.2")
145+
@click.version_option(version="0.4.4")
44146
def cli():
45147
"""
46148
IDP CLI - Batch document processing for IDP Accelerator
@@ -64,6 +166,11 @@ def cli():
64166
@click.option(
65167
"--admin-email", help="Admin user email address (required for new stacks)"
66168
)
169+
@click.option(
170+
"--from-code",
171+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
172+
help="Deploy from local code by building with publish.py (path to project root)",
173+
)
67174
@click.option(
68175
"--template-url",
69176
help="URL to CloudFormation template in S3 (default: auto-selected based on region)",
@@ -93,11 +200,15 @@ def cli():
93200
)
94201
@click.option("--parameters", help="Additional parameters as key=value,key2=value2")
95202
@click.option("--wait", is_flag=True, help="Wait for stack creation to complete")
203+
@click.option(
204+
"--no-rollback", is_flag=True, help="Disable rollback on stack creation failure"
205+
)
96206
@click.option("--region", help="AWS region (optional)")
97207
def deploy(
98208
stack_name: str,
99209
pattern: str,
100210
admin_email: str,
211+
from_code: Optional[str],
101212
template_url: str,
102213
max_concurrent: int,
103214
log_level: str,
@@ -106,6 +217,7 @@ def deploy(
106217
custom_config: Optional[str],
107218
parameters: Optional[str],
108219
wait: bool,
220+
no_rollback: bool,
109221
region: Optional[str],
110222
):
111223
"""
@@ -119,9 +231,15 @@ def deploy(
119231
# Create new stack with Pattern 2
120232
idp-cli deploy --stack-name my-idp --pattern pattern-2 --admin-email user@example.com
121233
122-
# Update existing stack with local config file (NEW!)
234+
# Deploy from local code (NEW!)
235+
idp-cli deploy --stack-name my-idp --from-code . --pattern pattern-2 --admin-email user@example.com --wait
236+
237+
# Update existing stack with local config file
123238
idp-cli deploy --stack-name my-idp --custom-config ./my-config.yaml
124239
240+
# Update existing stack from local code
241+
idp-cli deploy --stack-name my-idp --from-code . --wait
242+
125243
# Update existing stack with custom settings
126244
idp-cli deploy --stack-name my-idp --max-concurrent 200 --wait
127245
@@ -131,6 +249,13 @@ def deploy(
131249
--parameters "DataRetentionInDays=90,ErrorThreshold=5"
132250
"""
133251
try:
252+
# Validate mutually exclusive options
253+
if from_code and template_url:
254+
console.print(
255+
"[red]✗ Error: Cannot specify both --from-code and --template-url[/red]"
256+
)
257+
sys.exit(1)
258+
134259
# Auto-detect region if not provided
135260
if not region:
136261
import boto3
@@ -142,8 +267,15 @@ def deploy(
142267
"Region could not be determined. Please specify --region or configure AWS_DEFAULT_REGION"
143268
)
144269

270+
# Handle deployment from local code
271+
template_path = None
272+
if from_code:
273+
template_path, template_url = _build_from_local_code(
274+
from_code, region, stack_name
275+
)
276+
145277
# Determine template URL (user-provided takes precedence)
146-
if not template_url:
278+
elif not template_url:
147279
if region in TEMPLATE_URLS:
148280
template_url = TEMPLATE_URLS[region]
149281
console.print(f"[bold]Using template for region: {region}[/bold]")
@@ -224,12 +356,24 @@ def deploy(
224356

225357
# Deploy stack
226358
with console.status("[bold green]Deploying stack..."):
227-
result = deployer.deploy_stack(
228-
stack_name=stack_name,
229-
template_url=template_url,
230-
parameters=cfn_parameters,
231-
wait=wait,
232-
)
359+
if template_path:
360+
# Deploy from local template (built from code)
361+
result = deployer.deploy_stack(
362+
stack_name=stack_name,
363+
template_path=template_path,
364+
parameters=cfn_parameters,
365+
wait=wait,
366+
no_rollback=no_rollback,
367+
)
368+
else:
369+
# Deploy from template URL
370+
result = deployer.deploy_stack(
371+
stack_name=stack_name,
372+
template_url=template_url,
373+
parameters=cfn_parameters,
374+
wait=wait,
375+
no_rollback=no_rollback,
376+
)
233377

234378
# Show results
235379
# Success if operation completed successfully OR was successfully initiated

idp_cli/idp_cli/deployer.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def deploy_stack(
4141
template_url: Optional[str] = None,
4242
parameters: Dict[str, str] = None,
4343
wait: bool = False,
44+
no_rollback: bool = False,
4445
) -> Dict:
4546
"""
4647
Deploy CloudFormation stack
@@ -51,6 +52,7 @@ def deploy_stack(
5152
template_url: URL to CloudFormation template in S3 (optional)
5253
parameters: Stack parameters
5354
wait: Whether to wait for stack creation to complete
55+
no_rollback: If True, disable rollback on failure (DO_NOTHING)
5456
5557
Returns:
5658
Dictionary with deployment result
@@ -67,7 +69,18 @@ def deploy_stack(
6769
else:
6870
# Read template from local file
6971
template_body = self._read_template(template_path)
70-
template_param = {"TemplateBody": template_body}
72+
73+
# Check template size - CloudFormation has a 51,200 byte limit for direct upload
74+
template_size = len(template_body.encode("utf-8"))
75+
if template_size > 51200: # 50KB limit
76+
logger.info(
77+
f"Template size ({template_size} bytes) exceeds limit, uploading to S3"
78+
)
79+
template_url = self._upload_template_to_s3(template_body, stack_name)
80+
template_param = {"TemplateURL": template_url}
81+
logger.info(f"Template uploaded to: {template_url}")
82+
else:
83+
template_param = {"TemplateBody": template_body}
7184

7285
# Convert parameters dict to CloudFormation format
7386
cfn_parameters = [
@@ -94,6 +107,8 @@ def deploy_stack(
94107
operation = "UPDATE"
95108
else:
96109
logger.info(f"Creating new stack: {stack_name}")
110+
# Set OnFailure based on no_rollback flag
111+
on_failure = "DO_NOTHING" if no_rollback else "ROLLBACK"
97112
response = self.cfn.create_stack(
98113
StackName=stack_name,
99114
**template_param,
@@ -103,7 +118,7 @@ def deploy_stack(
103118
"CAPABILITY_NAMED_IAM",
104119
"CAPABILITY_AUTO_EXPAND",
105120
],
106-
OnFailure="ROLLBACK",
121+
OnFailure=on_failure,
107122
)
108123
operation = "CREATE"
109124

@@ -136,6 +151,31 @@ def _read_template(self, template_path: str) -> str:
136151

137152
return template_file.read_text()
138153

154+
def _upload_template_to_s3(self, template_body: str, stack_name: str) -> str:
155+
"""Upload template to S3 and return URL"""
156+
from datetime import datetime
157+
158+
import boto3
159+
160+
# Get or create bucket for templates
161+
bucket_name = get_or_create_config_bucket(self.region)
162+
163+
# Generate unique key
164+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
165+
s3_key = f"idp-cli/templates/{stack_name}_{timestamp}.yaml"
166+
167+
# Upload template
168+
s3 = boto3.client("s3", region_name=self.region)
169+
s3.put_object(
170+
Bucket=bucket_name,
171+
Key=s3_key,
172+
Body=template_body.encode("utf-8"),
173+
ServerSideEncryption="AES256",
174+
)
175+
176+
# Return S3 URL
177+
return f"https://s3.{self.region}.amazonaws.com/{bucket_name}/{s3_key}"
178+
139179
def _stack_exists(self, stack_name: str) -> bool:
140180
"""Check if stack exists"""
141181
try:

idp_cli/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
77

88
[project]
99
name = "idp-cli"
10-
version = "0.4.2"
10+
version = "0.4.4"
1111
description = "Command-line interface for IDP Accelerator batch document processing"
1212
authors = [{name = "AWS"}]
1313
license = {text = "MIT-0"}

0 commit comments

Comments
 (0)