exchange-keyshare 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exchange_keyshare/cli.py +4 -0
- exchange_keyshare/commands/config.py +34 -0
- exchange_keyshare/commands/setup.py +1 -0
- exchange_keyshare/commands/teardown.py +208 -0
- exchange_keyshare/config.py +4 -0
- exchange_keyshare/setup.py +129 -1
- {exchange_keyshare-0.1.0.dist-info → exchange_keyshare-0.1.1.dist-info}/METADATA +2 -1
- exchange_keyshare-0.1.1.dist-info/RECORD +17 -0
- exchange_keyshare-0.1.0.dist-info/RECORD +0 -15
- {exchange_keyshare-0.1.0.dist-info → exchange_keyshare-0.1.1.dist-info}/WHEEL +0 -0
- {exchange_keyshare-0.1.0.dist-info → exchange_keyshare-0.1.1.dist-info}/entry_points.txt +0 -0
exchange_keyshare/cli.py
CHANGED
|
@@ -4,8 +4,10 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
|
+
from exchange_keyshare.commands.config import config
|
|
7
8
|
from exchange_keyshare.commands.keys import keys
|
|
8
9
|
from exchange_keyshare.commands.setup import setup
|
|
10
|
+
from exchange_keyshare.commands.teardown import teardown
|
|
9
11
|
from exchange_keyshare.config import Config
|
|
10
12
|
|
|
11
13
|
|
|
@@ -30,6 +32,8 @@ def main(ctx: click.Context, config_path: str | None) -> None:
|
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
main.add_command(setup)
|
|
35
|
+
main.add_command(config)
|
|
36
|
+
main.add_command(teardown)
|
|
33
37
|
main.add_command(keys)
|
|
34
38
|
|
|
35
39
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Config command for displaying current configuration."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from exchange_keyshare.config import Config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.pass_context
|
|
12
|
+
def config(ctx: click.Context) -> None:
|
|
13
|
+
"""Display current configuration."""
|
|
14
|
+
cfg: Config = ctx.obj["config"]
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
if not cfg.stack_name:
|
|
18
|
+
console.print("Not configured. Run [cyan]exchange-keyshare setup[/cyan] first.")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
table = Table(show_header=False, box=None)
|
|
22
|
+
table.add_column("Key", style="dim")
|
|
23
|
+
table.add_column("Value", style="cyan")
|
|
24
|
+
|
|
25
|
+
table.add_row("Bucket", cfg.bucket or "")
|
|
26
|
+
table.add_row("Region", cfg.region or "")
|
|
27
|
+
table.add_row("Stack Name", cfg.stack_name or "")
|
|
28
|
+
table.add_row("Stack ID", cfg.stack_id or "")
|
|
29
|
+
table.add_row("Role ARN", cfg.role_arn or "")
|
|
30
|
+
table.add_row("External ID", cfg.external_id or "")
|
|
31
|
+
table.add_row("KMS Key ARN", cfg.kms_key_arn or "")
|
|
32
|
+
table.add_row("Config Path", str(cfg.config_path))
|
|
33
|
+
|
|
34
|
+
console.print(table)
|
|
@@ -155,6 +155,7 @@ def setup(ctx: click.Context) -> None:
|
|
|
155
155
|
config.bucket = result.bucket
|
|
156
156
|
config.region = result.region
|
|
157
157
|
config.stack_name = result.stack_name
|
|
158
|
+
config.stack_id = result.stack_id
|
|
158
159
|
config.role_arn = result.role_arn
|
|
159
160
|
config.external_id = result.external_id
|
|
160
161
|
config.kms_key_arn = result.kms_key_arn
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Teardown command for deleting AWS infrastructure."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console, Group, RenderableType
|
|
5
|
+
from rich.live import Live
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.spinner import Spinner
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from exchange_keyshare.config import Config
|
|
12
|
+
from exchange_keyshare.keys import CredentialInfo, list_credentials
|
|
13
|
+
from exchange_keyshare.setup import (
|
|
14
|
+
ResourceStatus,
|
|
15
|
+
StackProgress,
|
|
16
|
+
get_friendly_type,
|
|
17
|
+
get_stack_console_url,
|
|
18
|
+
poll_stack_deletion,
|
|
19
|
+
start_stack_deletion,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_delete_status_style(status: str) -> str:
|
|
24
|
+
"""Get rich style for a deletion status."""
|
|
25
|
+
if "DELETE_COMPLETE" in status or "DELETE_SKIPPED" in status:
|
|
26
|
+
return "green"
|
|
27
|
+
elif "IN_PROGRESS" in status:
|
|
28
|
+
return "yellow"
|
|
29
|
+
elif "FAILED" in status:
|
|
30
|
+
return "red"
|
|
31
|
+
return "white"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_delete_status_icon(status: str) -> str:
|
|
35
|
+
"""Get icon for a deletion status."""
|
|
36
|
+
if "DELETE_COMPLETE" in status or "DELETE_SKIPPED" in status:
|
|
37
|
+
return "[green]✓[/green]"
|
|
38
|
+
elif "IN_PROGRESS" in status:
|
|
39
|
+
return "[yellow]⋯[/yellow]"
|
|
40
|
+
elif "FAILED" in status:
|
|
41
|
+
return "[red]✗[/red]"
|
|
42
|
+
return " "
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_deletion_progress_display(progress: StackProgress) -> RenderableType:
|
|
46
|
+
"""Build a rich display showing resource deletion progress with spinner."""
|
|
47
|
+
table = Table(show_header=True, header_style="bold", box=None)
|
|
48
|
+
table.add_column("", width=2)
|
|
49
|
+
table.add_column("Resource", style="cyan", min_width=25)
|
|
50
|
+
table.add_column("Type", style="dim")
|
|
51
|
+
table.add_column("Status")
|
|
52
|
+
|
|
53
|
+
# Sort resources: in_progress first, then by name
|
|
54
|
+
def sort_key(item: tuple[str, ResourceStatus]) -> tuple[int, str]:
|
|
55
|
+
_, r = item
|
|
56
|
+
if "IN_PROGRESS" in r.status:
|
|
57
|
+
return (0, r.logical_id)
|
|
58
|
+
elif "COMPLETE" in r.status or "SKIPPED" in r.status:
|
|
59
|
+
return (2, r.logical_id)
|
|
60
|
+
else:
|
|
61
|
+
return (1, r.logical_id)
|
|
62
|
+
|
|
63
|
+
sorted_resources = sorted(progress.resources.items(), key=sort_key)
|
|
64
|
+
|
|
65
|
+
for _logical_id, resource in sorted_resources:
|
|
66
|
+
icon = get_delete_status_icon(resource.status)
|
|
67
|
+
style = get_delete_status_style(resource.status)
|
|
68
|
+
friendly_type = get_friendly_type(resource.resource_type)
|
|
69
|
+
|
|
70
|
+
# Simplify status text
|
|
71
|
+
status_text = resource.status.replace("_", " ").title()
|
|
72
|
+
|
|
73
|
+
table.add_row(
|
|
74
|
+
icon,
|
|
75
|
+
resource.logical_id,
|
|
76
|
+
friendly_type,
|
|
77
|
+
f"[{style}]{status_text}[/{style}]",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Add spinner header if still in progress
|
|
81
|
+
if not progress.is_complete and not progress.is_failed:
|
|
82
|
+
spinner = Spinner("dots", text=Text(" Deleting infrastructure...", style="bold"))
|
|
83
|
+
return Group(spinner, Text(""), table)
|
|
84
|
+
|
|
85
|
+
return table
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@click.command()
|
|
89
|
+
@click.pass_context
|
|
90
|
+
def teardown(ctx: click.Context) -> None:
|
|
91
|
+
"""Delete AWS infrastructure and revoke access.
|
|
92
|
+
|
|
93
|
+
This permanently deletes all infrastructure created by setup.
|
|
94
|
+
The S3 bucket and KMS key are retained but access is revoked.
|
|
95
|
+
"""
|
|
96
|
+
cfg: Config = ctx.obj["config"]
|
|
97
|
+
console = Console()
|
|
98
|
+
|
|
99
|
+
if not cfg.stack_name:
|
|
100
|
+
console.print("Nothing to teardown. Run [cyan]exchange-keyshare setup[/cyan] first.")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
region = cfg.region or "us-east-1"
|
|
104
|
+
|
|
105
|
+
# Try to list existing credentials (gracefully handle failure)
|
|
106
|
+
credentials: list[CredentialInfo] = []
|
|
107
|
+
if cfg.bucket and cfg.region:
|
|
108
|
+
try:
|
|
109
|
+
credentials = list_credentials(cfg.bucket, cfg.region)
|
|
110
|
+
except Exception:
|
|
111
|
+
pass # Gracefully ignore - permissions may already be revoked
|
|
112
|
+
|
|
113
|
+
# Build credential list for warning
|
|
114
|
+
cred_text = ""
|
|
115
|
+
if credentials:
|
|
116
|
+
cred_text = "\n[bold]Credentials that will become inaccessible:[/bold]\n"
|
|
117
|
+
for cred in credentials:
|
|
118
|
+
cred_text += f" - [cyan]{cred.exchange}[/cyan]\n"
|
|
119
|
+
if cred.pairs:
|
|
120
|
+
cred_text += f" Pairs: {', '.join(cred.pairs)}\n"
|
|
121
|
+
if cred.labels:
|
|
122
|
+
labels_str = ", ".join(f"{lbl['key']}={lbl['value']}" for lbl in cred.labels)
|
|
123
|
+
cred_text += f" Labels: {labels_str}\n"
|
|
124
|
+
cred_text += "\n"
|
|
125
|
+
|
|
126
|
+
# Build console URL if we have a stack ID
|
|
127
|
+
console_url = ""
|
|
128
|
+
if cfg.stack_id:
|
|
129
|
+
console_url = get_stack_console_url(cfg.stack_id, region)
|
|
130
|
+
|
|
131
|
+
# Show scary warning with specific resources
|
|
132
|
+
manual_cmd = f"aws cloudformation delete-stack --stack-name {cfg.stack_name} --region {region}"
|
|
133
|
+
|
|
134
|
+
warning = Panel(
|
|
135
|
+
"[bold red]WARNING: This action cannot be undone![/bold red]\n\n"
|
|
136
|
+
"[bold]Resources that will be DELETED:[/bold]\n"
|
|
137
|
+
f" - IAM Role: [cyan]{cfg.role_arn}[/cyan]\n"
|
|
138
|
+
f" - KMS Key Alias: [cyan]alias/{cfg.bucket}[/cyan]\n"
|
|
139
|
+
" - S3 Bucket Policies\n\n"
|
|
140
|
+
"[bold]Resources that will be RETAINED (but inaccessible):[/bold]\n"
|
|
141
|
+
f" - S3 Bucket: [cyan]{cfg.bucket}[/cyan]\n"
|
|
142
|
+
f" - KMS Key: [cyan]{cfg.kms_key_arn}[/cyan]\n"
|
|
143
|
+
f"{cred_text}\n"
|
|
144
|
+
f"Stack: [cyan]{cfg.stack_name}[/cyan]\n"
|
|
145
|
+
f"Region: [cyan]{region}[/cyan]\n\n"
|
|
146
|
+
f"[dim]Manual command: {manual_cmd}[/dim]",
|
|
147
|
+
title="[bold red]Teardown Infrastructure[/bold red]",
|
|
148
|
+
border_style="red",
|
|
149
|
+
)
|
|
150
|
+
console.print(warning)
|
|
151
|
+
console.print()
|
|
152
|
+
|
|
153
|
+
# Require typing the stack name to confirm
|
|
154
|
+
console.print(f"To confirm, type the stack name: [bold]{cfg.stack_name}[/bold]")
|
|
155
|
+
confirmation = click.prompt("", default="", show_default=False)
|
|
156
|
+
|
|
157
|
+
if confirmation != cfg.stack_name:
|
|
158
|
+
console.print("[yellow]Aborted.[/yellow] Stack name did not match.")
|
|
159
|
+
raise SystemExit(1)
|
|
160
|
+
|
|
161
|
+
console.print()
|
|
162
|
+
|
|
163
|
+
# Show console URL before starting deletion
|
|
164
|
+
if console_url:
|
|
165
|
+
console.print(f"[dim]Monitor progress in AWS Console:[/dim]")
|
|
166
|
+
console.print(f"[link={console_url}]{console_url}[/link]")
|
|
167
|
+
console.print()
|
|
168
|
+
console.print("[dim]You can safely cancel this command (Ctrl+C) - deletion will continue in AWS.[/dim]")
|
|
169
|
+
console.print()
|
|
170
|
+
|
|
171
|
+
# Start deletion and poll with live display
|
|
172
|
+
try:
|
|
173
|
+
cfn = start_stack_deletion(cfg.stack_name, region)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
console.print(f"[red]Error starting deletion: {e}[/red]")
|
|
176
|
+
raise SystemExit(1)
|
|
177
|
+
|
|
178
|
+
final_progress: StackProgress | None = None
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
with Live(console=console, refresh_per_second=10) as live:
|
|
182
|
+
for progress in poll_stack_deletion(cfg.stack_name, cfn):
|
|
183
|
+
display = build_deletion_progress_display(progress)
|
|
184
|
+
live.update(display)
|
|
185
|
+
final_progress = progress
|
|
186
|
+
|
|
187
|
+
if progress.is_complete or progress.is_failed:
|
|
188
|
+
break
|
|
189
|
+
except KeyboardInterrupt:
|
|
190
|
+
console.print()
|
|
191
|
+
console.print("[yellow]Interrupted.[/yellow] Deletion continues in AWS.")
|
|
192
|
+
if console_url:
|
|
193
|
+
console.print(f"[dim]Monitor progress:[/dim] {console_url}")
|
|
194
|
+
raise SystemExit(0)
|
|
195
|
+
|
|
196
|
+
if final_progress is None or final_progress.is_failed:
|
|
197
|
+
console.print()
|
|
198
|
+
console.print(f"[red]Error: {final_progress.failure_reason if final_progress else 'Unknown error'}[/red]")
|
|
199
|
+
if console_url:
|
|
200
|
+
console.print(f"[dim]Check AWS Console:[/dim] {console_url}")
|
|
201
|
+
raise SystemExit(1)
|
|
202
|
+
|
|
203
|
+
# Delete config file
|
|
204
|
+
if cfg.config_path.exists():
|
|
205
|
+
cfg.config_path.unlink()
|
|
206
|
+
|
|
207
|
+
console.print()
|
|
208
|
+
console.print("[green]Teardown complete.[/green] All access has been revoked.")
|
exchange_keyshare/config.py
CHANGED
|
@@ -26,6 +26,7 @@ class Config:
|
|
|
26
26
|
bucket: str | None = None
|
|
27
27
|
region: str | None = None
|
|
28
28
|
stack_name: str | None = None
|
|
29
|
+
stack_id: str | None = None
|
|
29
30
|
role_arn: str | None = None
|
|
30
31
|
external_id: str | None = None
|
|
31
32
|
kms_key_arn: str | None = None
|
|
@@ -36,6 +37,7 @@ class Config:
|
|
|
36
37
|
self.bucket = data.get("bucket")
|
|
37
38
|
self.region = data.get("region")
|
|
38
39
|
self.stack_name = data.get("stack_name")
|
|
40
|
+
self.stack_id = data.get("stack_id")
|
|
39
41
|
self.role_arn = data.get("role_arn")
|
|
40
42
|
self.external_id = data.get("external_id")
|
|
41
43
|
self.kms_key_arn = data.get("kms_key_arn")
|
|
@@ -49,6 +51,8 @@ class Config:
|
|
|
49
51
|
data["region"] = self.region
|
|
50
52
|
if self.stack_name:
|
|
51
53
|
data["stack_name"] = self.stack_name
|
|
54
|
+
if self.stack_id:
|
|
55
|
+
data["stack_id"] = self.stack_id
|
|
52
56
|
if self.role_arn:
|
|
53
57
|
data["role_arn"] = self.role_arn
|
|
54
58
|
if self.external_id:
|
exchange_keyshare/setup.py
CHANGED
|
@@ -40,6 +40,7 @@ class SetupResult:
|
|
|
40
40
|
role_arn: str
|
|
41
41
|
external_id: str
|
|
42
42
|
stack_name: str
|
|
43
|
+
stack_id: str
|
|
43
44
|
kms_key_arn: str
|
|
44
45
|
|
|
45
46
|
|
|
@@ -216,12 +217,138 @@ def _is_newer_status(new_status: str, old_status: str) -> bool:
|
|
|
216
217
|
return True
|
|
217
218
|
|
|
218
219
|
|
|
220
|
+
def get_stack_console_url(stack_id: str, region: str) -> str:
|
|
221
|
+
"""Get AWS Console URL for a CloudFormation stack."""
|
|
222
|
+
# URL-encode the stack ID (it contains special characters)
|
|
223
|
+
from urllib.parse import quote
|
|
224
|
+
encoded_stack_id = quote(stack_id, safe="")
|
|
225
|
+
return f"https://{region}.console.aws.amazon.com/cloudformation/home?region={region}#/stacks/stackinfo?stackId={encoded_stack_id}"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def start_stack_deletion(stack_name: str, region: str) -> "CloudFormationClient":
|
|
229
|
+
"""Start CloudFormation stack deletion. Returns cfn_client for polling."""
|
|
230
|
+
cfn = cast("CloudFormationClient", boto3.client("cloudformation", region_name=region)) # pyright: ignore[reportUnknownMemberType]
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
cfn.delete_stack(StackName=stack_name) # pyright: ignore[reportUnknownMemberType]
|
|
234
|
+
except ClientError as e:
|
|
235
|
+
raise Exception(f"Failed to delete stack: {e}") from e
|
|
236
|
+
|
|
237
|
+
return cfn
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def poll_stack_deletion(
|
|
241
|
+
stack_name: str,
|
|
242
|
+
cfn: "CloudFormationClient",
|
|
243
|
+
poll_interval: float = 2.0,
|
|
244
|
+
max_attempts: int = 300,
|
|
245
|
+
) -> Generator[StackProgress, None, None]:
|
|
246
|
+
"""Poll stack deletion progress and yield updates."""
|
|
247
|
+
resources: dict[str, ResourceStatus] = {}
|
|
248
|
+
|
|
249
|
+
for _ in range(max_attempts):
|
|
250
|
+
# Get current stack status
|
|
251
|
+
try:
|
|
252
|
+
stacks_response = cfn.describe_stacks(StackName=stack_name)
|
|
253
|
+
except ClientError as e:
|
|
254
|
+
# Stack not found means deletion complete
|
|
255
|
+
if "does not exist" in str(e):
|
|
256
|
+
yield StackProgress(
|
|
257
|
+
resources=resources,
|
|
258
|
+
stack_status="DELETE_COMPLETE",
|
|
259
|
+
is_complete=True,
|
|
260
|
+
is_failed=False,
|
|
261
|
+
)
|
|
262
|
+
return
|
|
263
|
+
raise
|
|
264
|
+
|
|
265
|
+
stack = stacks_response["Stacks"][0]
|
|
266
|
+
stack_status = stack.get("StackStatus", "UNKNOWN")
|
|
267
|
+
|
|
268
|
+
# Get resource events
|
|
269
|
+
try:
|
|
270
|
+
events_response = cfn.describe_stack_events(StackName=stack_name)
|
|
271
|
+
for event in events_response["StackEvents"]:
|
|
272
|
+
logical_id = event.get("LogicalResourceId", "")
|
|
273
|
+
resource_type = event.get("ResourceType", "")
|
|
274
|
+
status = event.get("ResourceStatus", "")
|
|
275
|
+
reason = event.get("ResourceStatusReason")
|
|
276
|
+
|
|
277
|
+
# Skip the stack itself
|
|
278
|
+
if resource_type == "AWS::CloudFormation::Stack":
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# Update if this is a newer status for this resource
|
|
282
|
+
if logical_id not in resources or _is_newer_delete_status(status, resources[logical_id].status):
|
|
283
|
+
resources[logical_id] = ResourceStatus(
|
|
284
|
+
logical_id=logical_id,
|
|
285
|
+
resource_type=resource_type,
|
|
286
|
+
status=status,
|
|
287
|
+
reason=reason,
|
|
288
|
+
)
|
|
289
|
+
except ClientError:
|
|
290
|
+
pass # Stack events may not be available
|
|
291
|
+
|
|
292
|
+
# Check for completion
|
|
293
|
+
is_complete = stack_status == "DELETE_COMPLETE"
|
|
294
|
+
is_failed = stack_status == "DELETE_FAILED"
|
|
295
|
+
|
|
296
|
+
failure_reason: str | None = None
|
|
297
|
+
if is_failed:
|
|
298
|
+
failed_resources = [r for r in resources.values() if "FAILED" in r.status]
|
|
299
|
+
if failed_resources:
|
|
300
|
+
reasons = [r.reason for r in failed_resources if r.reason]
|
|
301
|
+
failure_reason = "; ".join(reasons[:3]) if reasons else "Unknown error"
|
|
302
|
+
|
|
303
|
+
yield StackProgress(
|
|
304
|
+
resources=resources,
|
|
305
|
+
stack_status=stack_status,
|
|
306
|
+
is_complete=is_complete,
|
|
307
|
+
is_failed=is_failed,
|
|
308
|
+
failure_reason=failure_reason,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if is_complete or is_failed:
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
time.sleep(poll_interval)
|
|
315
|
+
|
|
316
|
+
# Timeout
|
|
317
|
+
yield StackProgress(
|
|
318
|
+
resources=resources,
|
|
319
|
+
stack_status="TIMEOUT",
|
|
320
|
+
is_complete=False,
|
|
321
|
+
is_failed=True,
|
|
322
|
+
failure_reason="Stack deletion timed out",
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _is_newer_delete_status(new_status: str, old_status: str) -> bool:
|
|
327
|
+
"""Check if new_status is more recent than old_status for deletion."""
|
|
328
|
+
status_order = [
|
|
329
|
+
"CREATE_COMPLETE",
|
|
330
|
+
"DELETE_IN_PROGRESS",
|
|
331
|
+
"DELETE_COMPLETE",
|
|
332
|
+
"DELETE_FAILED",
|
|
333
|
+
"DELETE_SKIPPED",
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
new_idx = status_order.index(new_status)
|
|
338
|
+
old_idx = status_order.index(old_status)
|
|
339
|
+
return new_idx > old_idx
|
|
340
|
+
except ValueError:
|
|
341
|
+
return True
|
|
342
|
+
|
|
343
|
+
|
|
219
344
|
def get_stack_outputs(stack_name: str, region: str) -> SetupResult:
|
|
220
345
|
"""Get outputs from a completed stack."""
|
|
221
346
|
cfn = cast("CloudFormationClient", boto3.client("cloudformation", region_name=region)) # pyright: ignore[reportUnknownMemberType]
|
|
222
347
|
|
|
223
348
|
response = cfn.describe_stacks(StackName=stack_name)
|
|
224
|
-
|
|
349
|
+
stack = response["Stacks"][0]
|
|
350
|
+
stack_id = stack.get("StackId", "")
|
|
351
|
+
stack_outputs = stack.get("Outputs", [])
|
|
225
352
|
outputs: dict[str, str] = {
|
|
226
353
|
o["OutputKey"]: o["OutputValue"]
|
|
227
354
|
for o in stack_outputs
|
|
@@ -234,6 +361,7 @@ def get_stack_outputs(stack_name: str, region: str) -> SetupResult:
|
|
|
234
361
|
role_arn=outputs["RoleArn"],
|
|
235
362
|
external_id=outputs["ExternalId"],
|
|
236
363
|
stack_name=stack_name,
|
|
364
|
+
stack_id=stack_id,
|
|
237
365
|
kms_key_arn=outputs["KmsKeyArn"],
|
|
238
366
|
)
|
|
239
367
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: exchange-keyshare
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: CLI for market makers to securely share exchange API credentials
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: boto3>=1.34.0
|
|
7
|
+
Requires-Dist: botocore[crt]>=1.34.0
|
|
7
8
|
Requires-Dist: click>=8.1.0
|
|
8
9
|
Requires-Dist: pyyaml>=6.0
|
|
9
10
|
Requires-Dist: questionary>=2.0.0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
exchange_keyshare/__init__.py,sha256=r2L4CpACTGPQTgruORVSNLxpD1vQD6zbcigQqDzzpt8,111
|
|
2
|
+
exchange_keyshare/cfn.py,sha256=Jr8QjeRHS9AP44vx14N_023we0iWjIHAz5Np9dnATe4,364
|
|
3
|
+
exchange_keyshare/cli.py,sha256=jYDKMvVtvm35Q5L1CNdNfuq_XF_6oq868nAK3Jg11no,987
|
|
4
|
+
exchange_keyshare/config.py,sha256=Db0hKKfBHISC-tnHp9blTSE2_lIuC1QdDxMuL3rATxs,2816
|
|
5
|
+
exchange_keyshare/keys.py,sha256=Lyl7vgJ4Bo0XA4iBKJyRWGd1ZpHPmrAuY7T0-sUWpOM,3278
|
|
6
|
+
exchange_keyshare/schema.py,sha256=BRv22tV3TifkoADVlMPGPNbhc9DKHtPq56s1EDcy70w,3818
|
|
7
|
+
exchange_keyshare/setup.py,sha256=Po_SgT_bEwE8LdCxR_kJQa44MP4ba76I_hzz7jYFFAQ,12781
|
|
8
|
+
exchange_keyshare/commands/__init__.py,sha256=gQ6tnU0Rvm0-ESWFUBU-KDl5dpNOpUTG509hXOQQjwY,27
|
|
9
|
+
exchange_keyshare/commands/config.py,sha256=1MXPIMWiSy1KerWdr9r8y6P7aA2YQRwk4wWeF7duSR0,1065
|
|
10
|
+
exchange_keyshare/commands/keys.py,sha256=NX_BDl2-iWoLso5ep9kQNCjx5r7SszUg2MY62qJ-eHc,14610
|
|
11
|
+
exchange_keyshare/commands/setup.py,sha256=VXvCXkTwlEIYOyflh8IGpBJrf8QiTeGIq2Mb1RghFic,5765
|
|
12
|
+
exchange_keyshare/commands/teardown.py,sha256=hHS9k6FNpoF9Dk4zrIHlKJzTmDVLy89fg39ACs_jPU4,7514
|
|
13
|
+
exchange_keyshare/templates/stack.yaml,sha256=KdNEeTFHm24pxQbSKzWxi2VyyjkKBYB9mYlBvSA6gfQ,6456
|
|
14
|
+
exchange_keyshare-0.1.1.dist-info/METADATA,sha256=KZgCuuVxnzX8ikEaIcEMwicmj_ympXMZR4uRjUCmO2Q,342
|
|
15
|
+
exchange_keyshare-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
exchange_keyshare-0.1.1.dist-info/entry_points.txt,sha256=4SeV5jhxW3C6plg8WefZdLhPBRqmYErdKfnGTUEWmmo,65
|
|
17
|
+
exchange_keyshare-0.1.1.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
exchange_keyshare/__init__.py,sha256=r2L4CpACTGPQTgruORVSNLxpD1vQD6zbcigQqDzzpt8,111
|
|
2
|
-
exchange_keyshare/cfn.py,sha256=Jr8QjeRHS9AP44vx14N_023we0iWjIHAz5Np9dnATe4,364
|
|
3
|
-
exchange_keyshare/cli.py,sha256=ogQBvZmrbR18QPQ4RJW9nERac3JqO_DPnk4RgyVjORU,825
|
|
4
|
-
exchange_keyshare/config.py,sha256=QD2apmjj99J9j5svOVS209726pS7uK5rcaxTAgYYBj4,2668
|
|
5
|
-
exchange_keyshare/keys.py,sha256=Lyl7vgJ4Bo0XA4iBKJyRWGd1ZpHPmrAuY7T0-sUWpOM,3278
|
|
6
|
-
exchange_keyshare/schema.py,sha256=BRv22tV3TifkoADVlMPGPNbhc9DKHtPq56s1EDcy70w,3818
|
|
7
|
-
exchange_keyshare/setup.py,sha256=n0Rn-gl8tFautc34HnA5lm-R0iCx7SAQAc7LTL9Zyn8,8235
|
|
8
|
-
exchange_keyshare/commands/__init__.py,sha256=gQ6tnU0Rvm0-ESWFUBU-KDl5dpNOpUTG509hXOQQjwY,27
|
|
9
|
-
exchange_keyshare/commands/keys.py,sha256=NX_BDl2-iWoLso5ep9kQNCjx5r7SszUg2MY62qJ-eHc,14610
|
|
10
|
-
exchange_keyshare/commands/setup.py,sha256=GTfFP4H6JJtIah47ruSlETLy9mJm6XvZbUpEN-OUMo0,5727
|
|
11
|
-
exchange_keyshare/templates/stack.yaml,sha256=KdNEeTFHm24pxQbSKzWxi2VyyjkKBYB9mYlBvSA6gfQ,6456
|
|
12
|
-
exchange_keyshare-0.1.0.dist-info/METADATA,sha256=B4dXgTPHXQgLBWuUJN-h7mzUedSK72-RlT4pKoNAL-c,305
|
|
13
|
-
exchange_keyshare-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
-
exchange_keyshare-0.1.0.dist-info/entry_points.txt,sha256=4SeV5jhxW3C6plg8WefZdLhPBRqmYErdKfnGTUEWmmo,65
|
|
15
|
-
exchange_keyshare-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|