nate2-scripts 1.0.0__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.
@@ -0,0 +1,514 @@
1
+ import argparse
2
+ import re
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from typing import NoReturn
6
+ import boto3
7
+ from botocore.exceptions import ClientError
8
+ import urllib.parse
9
+
10
+
11
+ @dataclass
12
+ class ParsedArn:
13
+ arn: str
14
+ partition: str
15
+ service: str
16
+ region: str
17
+ account: str
18
+ resource_type: str
19
+ resource_id: str
20
+
21
+
22
+ def split(arn: str | None, num: int, chr=":") -> list[str | None]:
23
+ return ((arn.split(chr, num) if arn else []) + [None] * 10)[: num + 1]
24
+
25
+
26
+ class ArnResolver:
27
+ # arn:aws:elasticloadbalancing:us-east-1:905502466771:loadbalancer/net/nate2-smb-port-forward/08360f60575c36cd
28
+ account: str
29
+ mdn: str
30
+
31
+ def __init__(self, account: str, mdn: str):
32
+ self.account = account
33
+ self.mdn = mdn
34
+
35
+ def try_parse_arn(self, service: str) -> str | None:
36
+ arn, rest = split(service, 1)
37
+ if arn != "arn" or not rest:
38
+ return None
39
+
40
+ return self._parse_aws(rest)
41
+
42
+ def _parse_aws(self, arn: str) -> str:
43
+ aws_loc, rest = arn.split(":", 1)
44
+ if aws_loc == "aws" and rest:
45
+ return self._parse_region(rest)
46
+
47
+ print(f"Error: Invalid ARN format: 'arn:{aws_loc}:*'", file=sys.stderr)
48
+ sys.exit(1)
49
+
50
+ def _parse_region(self, arn: str) -> str:
51
+ service, region, account, rest = arn.split(":", 3)
52
+
53
+ # global resources like S3 dont have a region or account
54
+ match service:
55
+ case "s3":
56
+ return self.parse_s3(rest)
57
+
58
+ if account != self.account:
59
+ print(
60
+ f"Error: current account {self.account} cannot access: 'arn:aws:{service}:{region}:{account}:*'",
61
+ file=sys.stderr,
62
+ )
63
+ sys.exit(1)
64
+
65
+ match service:
66
+ case "iam":
67
+ return self.parse_iam(account, rest)
68
+
69
+ if not region:
70
+ print(f"Error: Invalid ARN format: 'arn:aws:{service}::*'", file=sys.stderr)
71
+ sys.exit(1)
72
+
73
+ # require both region and account
74
+ match service:
75
+ case "ec2":
76
+ return self.parse_ec2(account, region, rest)
77
+ case "lambda":
78
+ return self.parse_lambda(account, region, rest)
79
+ case "logs":
80
+ return self.parse_logs(account, region, rest)
81
+ case "rds":
82
+ return self.parse_rds(account, region, rest)
83
+ case "secretsmanager":
84
+ return self.parse_secretsmanager(account, region, rest)
85
+ case "ecs":
86
+ return self.parse_ecs(account, region, rest)
87
+ case "eks":
88
+ return self.parse_eks(account, region, rest)
89
+ case "sns":
90
+ return self.parse_sns(account, region, rest)
91
+ case "sqs":
92
+ return self.parse_sqs(account, region, rest)
93
+ case "batch":
94
+ return self.parse_batch(account, region, rest)
95
+ case "dynamodb":
96
+ return self.parse_dynamodb(account, region, rest)
97
+ case "states":
98
+ return self.parse_states(account, region, rest)
99
+ case "elasticloadbalancing":
100
+ return self.parse_elb(account, region, rest)
101
+ # unchecked
102
+ case _:
103
+ print(
104
+ f"Error: Unsupported service '{service}' in ARN: 'arn:aws:{service}:*",
105
+ file=sys.stderr,
106
+ )
107
+ sys.exit(1)
108
+
109
+ return region
110
+
111
+ def parse_s3(self, rest) -> str:
112
+ resource_id = rest
113
+ try:
114
+ boto3.client("s3").head_bucket(Bucket=resource_id)
115
+ return f"https://{self.mdn}us-east-1.console.aws.amazon.com/s3/buckets/{resource_id}"
116
+ except ClientError as e:
117
+ print(
118
+ f"Error: Unable to access S3 bucket '{resource_id}' on account {self.account}: {e}",
119
+ file=sys.stderr,
120
+ )
121
+ sys.exit(1)
122
+
123
+ def parse_iam(self, account, rest) -> str:
124
+ resource_type, resource_id = split(rest, 1, "/")
125
+ match resource_type:
126
+ case "role":
127
+ # service-linked roles have a path prefix (e.g. aws-service-role/svc/RoleName)
128
+ try:
129
+ role_name = resource_id.split("/")[-1] if resource_id else ""
130
+ boto3.client("iam").get_role(RoleName=role_name)
131
+ return f"https://{self.mdn}us-east-1.console.aws.amazon.com/iam/home#/roles/{role_name}"
132
+ except ClientError as e:
133
+ print(
134
+ f"Error: Unable to access IAM role '{resource_id}' on account {self.account}: {e}",
135
+ file=sys.stderr,
136
+ )
137
+ sys.exit(1)
138
+ case "user":
139
+ try:
140
+ boto3.client("iam").get_user(UserName=resource_id)
141
+ return f"https://{self.mdn}us-east-1.console.aws.amazon.com/iam/home#/users/{resource_id}"
142
+ except ClientError as e:
143
+ print(
144
+ f"Error: Unable to access IAM user '{resource_id}' on account {self.account}: {e}",
145
+ file=sys.stderr,
146
+ )
147
+ sys.exit(1)
148
+ case _:
149
+ print(
150
+ f"Error: Unsupported IAM resource type '{resource_type}' in ARN: 'arn:aws:iam::{account}:{resource_type}/{resource_id}'",
151
+ file=sys.stderr,
152
+ )
153
+ sys.exit(1)
154
+
155
+ def _access_fail(self, what: str, err) -> NoReturn:
156
+ print(
157
+ f"Error: Unable to access {what} on account {self.account}: {err}",
158
+ file=sys.stderr,
159
+ )
160
+ sys.exit(1)
161
+
162
+ def _unsupported(self, service: str, resource_type, rest) -> NoReturn:
163
+ print(
164
+ f"Error: Unsupported {service} resource type '{resource_type}' in ARN: 'arn:aws:{service}:*:{rest}'",
165
+ file=sys.stderr,
166
+ )
167
+ sys.exit(1)
168
+
169
+ def parse_ec2(self, account, region, rest) -> str:
170
+ resource_type, resource_id = split(rest, 1, "/")
171
+ client = boto3.client("ec2", region_name=region)
172
+ base = f"https://{self.mdn}{region}.console.aws.amazon.com"
173
+ try:
174
+ match resource_type:
175
+ # not useful as I couldn't find it in console but
176
+ # I am keeping it here in case it becomes useful later
177
+ # case "vpc":
178
+ # result = client.describe_vpcs(VpcIds=[resource_id])
179
+ # if not result["Vpcs"]:
180
+ # raise ValueError(f"VPC '{resource_id}' not found.")
181
+ # return f"{base}/vpcconsole/home?region={region}#VpcDetails:VpcId={resource_id}"
182
+ # case "security-group":
183
+ # result = client.describe_security_groups(GroupIds=[resource_id])
184
+ # if not result["SecurityGroups"]:
185
+ # raise ValueError(f"Security group '{resource_id}' not found.")
186
+ # return f"{base}/ec2/home?region={region}#SecurityGroup:groupId={resource_id}"
187
+ # case "volume":
188
+ # result = client.describe_volumes(VolumeIds=[resource_id])
189
+ # if not result["Volumes"]:
190
+ # raise ValueError(f"Volume '{resource_id}' not found.")
191
+ # return f"{base}/ec2/home?region={region}#VolumeDetails:volumeId={resource_id}"
192
+ # case "image":
193
+ # result = client.describe_images(ImageIds=[resource_id])
194
+ # if not result["Images"]:
195
+ # raise ValueError(f"AMI '{resource_id}' not found.")
196
+ # return f"{base}/ec2/home?region={region}#ImageDetails:imageId={resource_id}"
197
+ # case "launch-template":
198
+ # result = client.describe_launch_templates(
199
+ # LaunchTemplateIds=[resource_id]
200
+ # )
201
+ # if not result["LaunchTemplates"]:
202
+ # raise ValueError(f"Launch template '{resource_id}' not found.")
203
+ # return f"{base}/ec2/home?region={region}#LaunchTemplateDetails:launchTemplateId={resource_id}"
204
+ # case "snapshot":
205
+ # result = client.describe_snapshots(SnapshotIds=[resource_id])
206
+ # if not result["Snapshots"]:
207
+ # raise ValueError(f"Snapshot '{resource_id}' not found.")
208
+ # return f"{base}/ec2/home?region={region}#SnapshotDetails:snapshotId={resource_id}"
209
+ # case "route-table":
210
+ # result = client.describe_route_tables(RouteTableIds=[resource_id])
211
+ # if not result["RouteTables"]:
212
+ # raise ValueError(f"Route table '{resource_id}' not found.")
213
+ # return f"{base}/vpcconsole/home?region={region}#RouteTableDetails:RouteTableId={resource_id}"
214
+ # case "network-interface":
215
+ # result = client.describe_network_interfaces(
216
+ # NetworkInterfaceIds=[resource_id]
217
+ # )
218
+ # if not result["NetworkInterfaces"]:
219
+ # raise ValueError(
220
+ # f"Network interface '{resource_id}' not found."
221
+ # )
222
+ # return f"{base}/ec2/home?region={region}#NetworkInterface:networkInterfaceId={resource_id}"
223
+ case "instance":
224
+ result = client.describe_instances(InstanceIds=[resource_id])
225
+ if not result["Reservations"]:
226
+ raise ValueError(f"Instance '{resource_id}' not found.")
227
+ return f"{base}/ec2/home?region={region}#InstanceDetails:instanceId={resource_id}"
228
+ case "subnet":
229
+ result = client.describe_subnets(SubnetIds=[resource_id])
230
+ if not result["Subnets"]:
231
+ raise ValueError(f"Subnet '{resource_id}' not found.")
232
+ return f"{base}/vpcconsole/home?region={region}#SubnetDetails:subnetId={resource_id}"
233
+ case "natgateway":
234
+ result = client.describe_nat_gateways(NatGatewayIds=[resource_id])
235
+ if not result["NatGateways"]:
236
+ raise ValueError(f"NAT gateway '{resource_id}' not found.")
237
+ return f"{base}/vpcconsole/home?region={region}#NatGatewayDetails:natGatewayId={resource_id}"
238
+ case _:
239
+ self._unsupported("ec2", resource_type, rest)
240
+ except ClientError as e:
241
+ self._access_fail(f"EC2 {resource_type} '{resource_id}'", e)
242
+ except ValueError as e:
243
+ self._access_fail(f"EC2 {resource_type} '{resource_id}'", e)
244
+
245
+ def parse_lambda(self, account, region, rest) -> str:
246
+ resource_type, resource_id = split(rest, 1, ":")
247
+ if resource_type != "function":
248
+ self._unsupported("lambda", resource_type, rest)
249
+ try:
250
+ boto3.client("lambda", region_name=region).get_function(
251
+ FunctionName=resource_id
252
+ )
253
+ return f"https://{self.mdn}{region}.console.aws.amazon.com/lambda/home?region={region}#/functions/{resource_id}"
254
+ except ClientError as e:
255
+ self._access_fail(f"Lambda function '{resource_id}'", e)
256
+
257
+ def parse_logs(self, account, region, rest) -> str:
258
+ resource_type, resource_id = split(rest, 1, ":")
259
+ if resource_type != "log-group":
260
+ self._unsupported("logs", resource_type, rest)
261
+ # log-group arns often carry a trailing ':*' wildcard
262
+ name = resource_id or ""
263
+ if name.endswith(":*"):
264
+ name = name[:-2]
265
+ try:
266
+ result = boto3.client("logs", region_name=region).describe_log_groups(
267
+ logGroupNamePrefix=name
268
+ )
269
+ if not any(
270
+ lg["logGroupName"] == name for lg in result.get("logGroups", [])
271
+ ):
272
+ raise ValueError(f"Log group '{name}' not found.")
273
+ encoded = urllib.parse.quote(name, safe="")
274
+ return f"https://{self.mdn}{region}.console.aws.amazon.com/cloudwatch/home?region={region}#logsV2:log-groups/log-group/{encoded}"
275
+ except ClientError as e:
276
+ self._access_fail(f"log group '{name}'", e)
277
+ except ValueError as e:
278
+ self._access_fail(f"log group '{name}'", e)
279
+
280
+ def parse_rds(self, account, region, rest) -> str:
281
+ resource_type, resource_id = split(rest, 1, ":")
282
+ client = boto3.client("rds", region_name=region)
283
+ base = f"https://{self.mdn}{region}.console.aws.amazon.com/rds/home?region={region}"
284
+ try:
285
+ match resource_type:
286
+ case "db":
287
+ result = client.describe_db_instances(
288
+ DBInstanceIdentifier=resource_id
289
+ )
290
+ if not result["DBInstances"]:
291
+ raise ValueError(f"RDS instance '{resource_id}' not found.")
292
+ return f"{base}#database:id={resource_id};is-cluster=false"
293
+ case "cluster":
294
+ result = client.describe_db_clusters(
295
+ DBClusterIdentifier=resource_id
296
+ )
297
+ if not result["DBClusters"]:
298
+ raise ValueError(f"RDS cluster '{resource_id}' not found.")
299
+ return f"{base}#database:id={resource_id};is-cluster=true"
300
+ case _:
301
+ self._unsupported("rds", resource_type, rest)
302
+ except ClientError as e:
303
+ self._access_fail(f"RDS {resource_type} '{resource_id}'", e)
304
+ except ValueError as e:
305
+ self._access_fail(f"RDS {resource_type} '{resource_id}'", e)
306
+
307
+ def parse_secretsmanager(self, account, region, rest) -> str:
308
+ resource_type, resource_id = split(rest, 1, ":")
309
+ if resource_type != "secret":
310
+ self._unsupported("secretsmanager", resource_type, rest)
311
+ arn = f"arn:aws:secretsmanager:{region}:{account}:secret:{resource_id}"
312
+ try:
313
+ boto3.client("secretsmanager", region_name=region).describe_secret(
314
+ SecretId=arn
315
+ )
316
+ # secret arns end with a random 6-char suffix that the console omits
317
+ name = re.sub(r"-[A-Za-z0-9]{6}$", "", resource_id or "")
318
+ encoded = urllib.parse.quote(name, safe="")
319
+ return f"https://{self.mdn}{region}.console.aws.amazon.com/secretsmanager/home?region={region}#!/secret?name={encoded}"
320
+ except ClientError as e:
321
+ self._access_fail(f"secret '{resource_id}'", e)
322
+
323
+ def parse_ecs(self, account, region, rest) -> str:
324
+ resource_type, resource_id = split(rest, 1, "/")
325
+ client = boto3.client("ecs", region_name=region)
326
+ base = f"https://{self.mdn}{region}.console.aws.amazon.com/ecs/v2/clusters"
327
+ try:
328
+ match resource_type:
329
+ case "cluster":
330
+ result = client.describe_clusters(clusters=[resource_id])
331
+ active = [
332
+ c for c in result.get("clusters", []) if c["status"] == "ACTIVE"
333
+ ]
334
+ if not active:
335
+ raise ValueError(
336
+ f"ECS cluster '{resource_id}' not found or not active."
337
+ )
338
+ return f"{base}/{resource_id}/services?region={region}"
339
+ # TODO test this
340
+ case "service":
341
+ cluster, service_name = split(resource_id, 1, "/")
342
+ result = client.describe_services(
343
+ cluster=cluster, services=[service_name]
344
+ )
345
+ active = [
346
+ s for s in result.get("services", []) if s["status"] == "ACTIVE"
347
+ ]
348
+ if not active:
349
+ raise ValueError(
350
+ f"ECS service '{service_name}' in cluster '{cluster}' not found or not active."
351
+ )
352
+ return f"{base}/{cluster}/services/{service_name}?region={region}"
353
+ # TODO test this
354
+ case "task":
355
+ cluster, task_id = split(resource_id, 1, "/")
356
+ result = client.describe_tasks(cluster=cluster, tasks=[task_id])
357
+ if not result.get("tasks"):
358
+ raise ValueError(
359
+ f"ECS task '{task_id}' in cluster '{cluster}' not found."
360
+ )
361
+ return f"{base}/{cluster}/tasks/{task_id}?region={region}"
362
+ case "task-definition":
363
+ family, revision = split(resource_id, 1, ":")
364
+ td = f"{family}:{revision}" if revision else family
365
+ result = client.describe_task_definition(taskDefinition=td)
366
+ if not result.get("taskDefinition"):
367
+ raise ValueError(f"ECS task definition '{td}' not found.")
368
+ base_td = f"https://{self.mdn}{region}.console.aws.amazon.com/ecs/v2/task-definitions"
369
+ if revision:
370
+ return f"{base_td}/{family}/{revision}?region={region}"
371
+ return f"{base_td}/{family}?region={region}"
372
+ case _:
373
+ self._unsupported("ecs", resource_type, rest)
374
+ except ClientError as e:
375
+ self._access_fail(f"ECS {resource_type} '{resource_id}'", e)
376
+ except ValueError as e:
377
+ self._access_fail(f"ECS {resource_type} '{resource_id}'", e)
378
+
379
+ def parse_eks(self, account, region, rest) -> str:
380
+ resource_type, resource_id = split(rest, 1, "/")
381
+ if resource_type != "cluster":
382
+ self._unsupported("eks", resource_type, rest)
383
+ try:
384
+ boto3.client("eks", region_name=region).describe_cluster(name=resource_id)
385
+ return f"https://{self.mdn}{region}.console.aws.amazon.com/eks/home?region={region}#/clusters/{resource_id}"
386
+ except ClientError as e:
387
+ self._access_fail(f"EKS cluster '{resource_id}'", e)
388
+
389
+ def parse_sns(self, account, region, rest) -> str:
390
+ topic = rest
391
+ arn = f"arn:aws:sns:{region}:{account}:{topic}"
392
+ try:
393
+ boto3.client("sns", region_name=region).get_topic_attributes(TopicArn=arn)
394
+ return f"https://{self.mdn}{region}.console.aws.amazon.com/sns/v3/home?region={region}#/topic/{arn}"
395
+ except ClientError as e:
396
+ self._access_fail(f"SNS topic '{topic}'", e)
397
+
398
+ def parse_sqs(self, account, region, rest) -> str:
399
+ queue = rest
400
+ try:
401
+ url = boto3.client("sqs", region_name=region).get_queue_url(
402
+ QueueName=queue, QueueOwnerAWSAccountId=account
403
+ )["QueueUrl"]
404
+ encoded = urllib.parse.quote(url, safe="")
405
+ return f"https://{self.mdn}{region}.console.aws.amazon.com/sqs/v3/home?region={region}#/queues/{encoded}"
406
+ except ClientError as e:
407
+ self._access_fail(f"SQS queue '{queue}'", e)
408
+
409
+ def parse_batch(self, account, region, rest) -> str:
410
+ resource_type, resource_id = split(rest, 1, "/")
411
+ client = boto3.client("batch", region_name=region)
412
+ base = f"https://{self.mdn}{region}.console.aws.amazon.com/batch/home?region={region}"
413
+ try:
414
+ match resource_type:
415
+ case "job-queue":
416
+ result = client.describe_job_queues(jobQueues=[resource_id])
417
+ if not result.get("jobQueues"):
418
+ raise ValueError(f"Batch job queue '{resource_id}' not found.")
419
+ arn = f"arn:aws:batch:{region}:{account}:job-queue/{resource_id}"
420
+ return f"{base}#queues/detail/{arn}"
421
+ case "compute-environment":
422
+ result = client.describe_compute_environments(
423
+ computeEnvironments=[resource_id]
424
+ )
425
+ if not result.get("computeEnvironments"):
426
+ raise ValueError(
427
+ f"Batch compute environment '{resource_id}' not found."
428
+ )
429
+ arn = f"arn:aws:batch:{region}:{account}:compute-environment/{resource_id}"
430
+ return f"{base}#compute-environments/detail/{arn}"
431
+ case "job-definition":
432
+ arn = (
433
+ f"arn:aws:batch:{region}:{account}:job-definition/{resource_id}"
434
+ )
435
+ result = client.describe_job_definitions(jobDefinitions=[arn])
436
+ if not result.get("jobDefinitions"):
437
+ raise ValueError(
438
+ f"Batch job definition '{resource_id}' not found."
439
+ )
440
+ return f"{base}#job-definition/detail/{arn}"
441
+ # TODO TEST THIS
442
+ case "job":
443
+ result = client.describe_jobs(jobs=[resource_id])
444
+ if not result.get("jobs"):
445
+ raise ValueError(f"Batch job '{resource_id}' not found.")
446
+ return f"{base}#jobs/detail/{resource_id}"
447
+ case _:
448
+ self._unsupported("batch", resource_type, rest)
449
+ except ClientError as e:
450
+ self._access_fail(f"Batch {resource_type} '{resource_id}'", e)
451
+ except ValueError as e:
452
+ self._access_fail(f"Batch {resource_type} '{resource_id}'", e)
453
+
454
+ def parse_dynamodb(self, account, region, rest) -> str:
455
+ resource_type, resource_id = split(rest, 1, "/")
456
+ if resource_type != "table":
457
+ self._unsupported("dynamodb", resource_type, rest)
458
+ try:
459
+ boto3.client("dynamodb", region_name=region).describe_table(
460
+ TableName=resource_id
461
+ )
462
+ return f"https://{self.mdn}{region}.console.aws.amazon.com/dynamodbv2/home?region={region}#table?name={resource_id}"
463
+ except ClientError as e:
464
+ self._access_fail(f"DynamoDB table '{resource_id}'", e)
465
+
466
+ def parse_states(self, account, region, rest) -> str:
467
+ resource_type, resource_id = split(rest, 1, ":")
468
+ client = boto3.client("stepfunctions", region_name=region)
469
+ base = f"https://{self.mdn}{region}.console.aws.amazon.com/states/home?region={region}"
470
+ try:
471
+ match resource_type:
472
+ case "stateMachine":
473
+ arn = (
474
+ f"arn:aws:states:{region}:{account}:stateMachine:{resource_id}"
475
+ )
476
+ client.describe_state_machine(stateMachineArn=arn)
477
+ return (
478
+ f"{base}#/statemachines/view/{urllib.parse.quote(arn, safe='')}"
479
+ )
480
+ # TODO test this
481
+ case "execution":
482
+ arn = f"arn:aws:states:{region}:{account}:execution:{resource_id}"
483
+ client.describe_execution(executionArn=arn)
484
+ return f"{base}#/v2/executions/details/{urllib.parse.quote(arn, safe='')}"
485
+ case _:
486
+ self._unsupported("states", resource_type, rest)
487
+ except ClientError as e:
488
+ self._access_fail(f"Step Functions {resource_type} '{resource_id}'", e)
489
+
490
+ def parse_elb(self, account, region, rest) -> str:
491
+ resource_type, resource_id = split(rest, 1, "/")
492
+ client = boto3.client("elbv2", region_name=region)
493
+ base = f"https://{self.mdn}{region}.console.aws.amazon.com/ec2/home?region={region}"
494
+ arn = f"arn:aws:elasticloadbalancing:{region}:{account}:{rest}"
495
+ try:
496
+ match resource_type:
497
+ # TODO test this
498
+ case "loadbalancer":
499
+ result = client.describe_load_balancers(LoadBalancerArns=[arn])
500
+ if not result.get("LoadBalancers"):
501
+ raise ValueError(f"Load balancer '{resource_id}' not found.")
502
+ return f"{base}#LoadBalancers:loadBalancerArn={arn}"
503
+ # TODO test this
504
+ case "targetgroup":
505
+ result = client.describe_target_groups(TargetGroupArns=[arn])
506
+ if not result.get("TargetGroups"):
507
+ raise ValueError(f"Target group '{resource_id}' not found.")
508
+ return f"{base}#TargetGroup:targetGroupArn={arn}"
509
+ case _:
510
+ self._unsupported("elasticloadbalancing", resource_type, rest)
511
+ except ClientError as e:
512
+ self._access_fail(f"ELB {resource_type} '{resource_id}'", e)
513
+ except ValueError as e:
514
+ self._access_fail(f"ELB {resource_type} '{resource_id}'", e)
@@ -0,0 +1,104 @@
1
+ import argparse
2
+ import re
3
+ import sys
4
+ from dataclasses import dataclass
5
+ import boto3
6
+ from nate2_scripts.console.arnResolver import ArnResolver
7
+ from nate2_scripts.console.regionCache import DEFAULT_REGION, RegionCache
8
+ from botocore.exceptions import ClientError
9
+ import urllib.parse
10
+
11
+ from nate2_scripts.console.resourceIdResolver import ResourceIdResolver
12
+
13
+
14
+ class DestinationResolver:
15
+
16
+ account: str
17
+ region_cache: RegionCache
18
+ mdn: str
19
+
20
+ def __init__(self, account: str, region_cache: RegionCache, multisession_domain_name: str):
21
+ self.account = account
22
+ self.region_cache = region_cache
23
+ self.mdn = multisession_domain_name
24
+
25
+ @staticmethod
26
+ def get_service_list() -> list[str]:
27
+ return list(DestinationResolver._get_services("").keys())
28
+
29
+ def parse_destination(self, service: str, region: str) -> str:
30
+ default_region = (
31
+ region if len(region) > 0 else self.region_cache.get_default_region()
32
+ )
33
+
34
+ service_urls = DestinationResolver._get_services(default_region, self.mdn)
35
+
36
+ if not service:
37
+ return f"https://{self.mdn}{default_region}.console.aws.amazon.com/"
38
+
39
+ arn_dest = ArnResolver(self.account,self.mdn).try_parse_arn(service)
40
+ if arn_dest:
41
+ return arn_dest
42
+
43
+ id_dest = ResourceIdResolver(self.region_cache, self.mdn).try_parse_resource_id(service)
44
+ if id_dest:
45
+ return id_dest
46
+
47
+ if service.lower() not in service_urls:
48
+ print(f"Error: Unknown service '{service}'.", file=sys.stderr)
49
+ sys.exit(1)
50
+ return service_urls[service.lower()]
51
+
52
+ @staticmethod
53
+ def _get_services(region: str, mdn: str = "") -> dict[str, str]:
54
+ return {
55
+ "ec2": f"https://{mdn}{region}.console.aws.amazon.com/ec2/",
56
+ "lambda": f"https://{mdn}{region}.console.aws.amazon.com/lambda/",
57
+ "s3": f"https://{mdn}{region}.console.aws.amazon.com/s3/",
58
+ "iam": f"https://{mdn}{region}.console.aws.amazon.com/iam/",
59
+ "cloudwatch": f"https://{mdn}{region}.console.aws.amazon.com/cloudwatch/",
60
+ "cw": f"https://{mdn}{region}.console.aws.amazon.com/cloudwatch/",
61
+ "logs": f"https://{mdn}{region}.console.aws.amazon.com/cloudwatch/home#logsV2:log-groups",
62
+ "cloudformation": f"https://{mdn}{region}.console.aws.amazon.com/cloudformation/",
63
+ "cf": f"https://{mdn}{region}.console.aws.amazon.com/cloudformation/",
64
+ "rds": f"https://{mdn}{region}.console.aws.amazon.com/rds/",
65
+ "ecs": f"https://{mdn}{region}.console.aws.amazon.com/ecs/",
66
+ "eks": f"https://{mdn}{region}.console.aws.amazon.com/eks/",
67
+ "vpc": f"https://{mdn}{region}.console.aws.amazon.com/vpc/",
68
+ "sg": f"https://{mdn}{region}.console.aws.amazon.com/vpcconsole/home#SecurityGroups",
69
+ "batch": f"https://{mdn}{region}.console.aws.amazon.com/batch/",
70
+ "sns": f"https://{mdn}{region}.console.aws.amazon.com/sns/",
71
+ "sqs": f"https://{mdn}{region}.console.aws.amazon.com/sqs/",
72
+ "step": f"https://{mdn}{region}.console.aws.amazon.com/states/",
73
+ "cost": f"https://{mdn}{region}.console.aws.amazon.com/costmanagement/",
74
+ "tennex": f"https://{mdn}{region}.console.aws.amazon.com/marketplace/search?applicationId=AWS-Marketplace-Console&ref_=ucaf&text=tennex",
75
+ "ecr": f"https://{mdn}{region}.console.aws.amazon.com/ecr/",
76
+ "support": f"https://{mdn}{region}.console.aws.amazon.com/support",
77
+ "dynamo": f"https://{mdn}{region}.console.aws.amazon.com/dynamodb/",
78
+ "bedrock": f"https://{mdn}{region}.console.aws.amazon.com/bedrock/",
79
+ "br": f"https://{mdn}{region}.console.aws.amazon.com/bedrock/",
80
+ "config": f"https://{mdn}{region}.console.aws.amazon.com/config/",
81
+ "controltower": f"https://{mdn}{region}.console.aws.amazon.com/controltower/",
82
+ "cognito": f"https://{mdn}{region}.console.aws.amazon.com/cognito/",
83
+ "cloudfront": f"https://{mdn}{region}.console.aws.amazon.com/cloudfront/",
84
+ # cant use cf because it conflicts with cloudformation and I think thats more important
85
+ "front": f"https://{mdn}{region}.console.aws.amazon.com/cloudfront/",
86
+ "route53": f"https://{mdn}{region}.console.aws.amazon.com/route53/",
87
+ "53": f"https://{mdn}{region}.console.aws.amazon.com/route53/",
88
+ "kms": f"https://{mdn}{region}.console.aws.amazon.com/kms/",
89
+ "secretsmanager": f"https://{mdn}{region}.console.aws.amazon.com/secretsmanager/",
90
+ # I mistyped it enough that this is here now
91
+ "secretmanager": f"https://{mdn}{region}.console.aws.amazon.com/secretsmanager/",
92
+ "sm": f"https://{mdn}{region}.console.aws.amazon.com/secretsmanager/",
93
+ "storagegateway": f"https://{mdn}{region}.console.aws.amazon.com/storagegateway/",
94
+ "sgw": f"https://{mdn}{region}.console.aws.amazon.com/storagegateway/",
95
+ "acl": f"https://{mdn}{region}.console.aws.amazon.com/vpcconsole/home#acls:",
96
+ "ami": f"https://{mdn}{region}.console.aws.amazon.com/ec2/home#Images",
97
+ "elb": f"https://{mdn}{region}.console.aws.amazon.com/ec2/home#LoadBalancers",
98
+ "athena": f"https://{mdn}{region}.console.aws.amazon.com/athena/home#/query-editor",
99
+ "cloudtrail": f"https://{mdn}{region}.console.aws.amazon.com/cloudtrailv2/",
100
+ "ct": f"https://{mdn}{region}.console.aws.amazon.com/cloudtrailv2/",
101
+ # unchecked
102
+ }
103
+
104
+