-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcli.py
More file actions
133 lines (106 loc) · 5.17 KB
/
Copy pathcli.py
File metadata and controls
133 lines (106 loc) · 5.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
"""DeployDiff CLI - infrastructure change preview with cost impact and rollback."""
from __future__ import annotations
import click
from rich.console import Console
from .cloudformation_parser import parse_cloudformation_changeset
from .cost_estimator import estimate_costs
from .diff_renderer import render_plan
from .models import CostEstimate, DeployPlan
from .pulumi_parser import parse_pulumi_preview
from .rollback import generate_rollback_commands
from .terraform_parser import parse_terraform_plan
console = Console()
@click.group()
@click.version_option(package_name="deploydiff")
def main():
"""DeployDiff - Preview infrastructure changes with cost impact and rollback."""
pass
@main.command()
@click.option("--tf", "terraform_file", type=click.Path(exists=True), help="Terraform plan JSON file")
@click.option("--cfn", "cloudformation_file", type=click.Path(exists=True), help="CloudFormation change set JSON file")
@click.option("--pulumi", "pulumi_file", type=click.Path(exists=True), help="Pulumi preview JSON file")
@click.option("-v", "--verbose", is_flag=True, help="Show before/after details for each change")
def preview(terraform_file, cloudformation_file, pulumi_file, verbose):
"""Preview infrastructure changes from a plan file."""
plan = _load_plan(terraform_file, cloudformation_file, pulumi_file)
if plan is None:
console.print("[red]Error: Provide one of --tf, --cfn, or --pulumi[/red]")
raise SystemExit(1)
render_plan(plan, console, verbose=verbose)
@main.command()
@click.option("--tf", "terraform_file", type=click.Path(exists=True), help="Terraform plan JSON file")
@click.option("--cfn", "cloudformation_file", type=click.Path(exists=True), help="CloudFormation change set JSON file")
@click.option("--pulumi", "pulumi_file", type=click.Path(exists=True), help="Pulumi preview JSON file")
@click.option("--pricing", "pricing_file", type=click.Path(exists=True), help="Custom pricing JSON file")
def cost(terraform_file, cloudformation_file, pulumi_file, pricing_file):
"""Estimate monthly cost impact of infrastructure changes."""
plan = _load_plan(terraform_file, cloudformation_file, pulumi_file)
if plan is None:
console.print("[red]Error: Provide one of --tf, --cfn, or --pulumi[/red]")
raise SystemExit(1)
estimates = estimate_costs(plan, pricing_file=pricing_file)
_render_costs(estimates, plan, console)
@main.command()
@click.option("--tf", "terraform_file", type=click.Path(exists=True), help="Terraform plan JSON file")
@click.option("--cfn", "cloudformation_file", type=click.Path(exists=True), help="CloudFormation change set JSON file")
@click.option("--pulumi", "pulumi_file", type=click.Path(exists=True), help="Pulumi preview JSON file")
def rollback(terraform_file, cloudformation_file, pulumi_file):
"""Generate rollback commands for infrastructure changes."""
plan = _load_plan(terraform_file, cloudformation_file, pulumi_file)
if plan is None:
console.print("[red]Error: Provide one of --tf, --cfn, or --pulumi[/red]")
raise SystemExit(1)
commands = generate_rollback_commands(plan)
for cmd in commands:
console.print(cmd)
def _load_plan(
terraform_file: str | None,
cloudformation_file: str | None,
pulumi_file: str | None,
) -> DeployPlan | None:
"""Load a deployment plan from the specified file."""
sources = [terraform_file, cloudformation_file, pulumi_file]
provided = [s for s in sources if s is not None]
if len(provided) == 0:
return None
if len(provided) > 1:
console.print("[red]Error: Provide only one source file (--tf, --cfn, or --pulumi)[/red]")
raise SystemExit(1)
if terraform_file:
return parse_terraform_plan(terraform_file)
elif cloudformation_file:
return parse_cloudformation_changeset(cloudformation_file)
elif pulumi_file:
return parse_pulumi_preview(pulumi_file)
return None
def _render_costs(estimates: list[CostEstimate], plan: DeployPlan, console: Console) -> None:
"""Render cost estimates to the console."""
from rich.table import Table
from rich import box
table = Table(title="Cost Impact Estimate", box=box.ROUNDED, show_header=True)
table.add_column("Resource", style="bold")
table.add_column("Before ($/mo)", justify="right")
table.add_column("After ($/mo)", justify="right")
table.add_column("Delta ($/mo)", justify="right")
for est in estimates:
delta = est.monthly_delta
if delta > 0:
delta_str = f"[red]+${delta:.2f}[/red]"
elif delta < 0:
delta_str = f"[green]-${abs(delta):.2f}[/green]"
else:
delta_str = "$0.00"
table.add_row(
est.resource_address,
f"${est.monthly_cost_before:.2f}",
f"${est.monthly_cost_after:.2f}",
delta_str,
)
console.print(table)
total = plan.total_monthly_delta
if total > 0:
console.print(f"\n[bold red]Total monthly increase: +${total:.2f}[/bold red]")
elif total < 0:
console.print(f"\n[bold green]Total monthly decrease: -${abs(total):.2f}[/bold green]")
else:
console.print(f"\n[bold]Total monthly change: $0.00[/bold]")