qodalis-cli-server-plugin-aws 1.0.0b13__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.
- qodalis_cli_aws/__init__.py +5 -0
- qodalis_cli_aws/aws_module.py +34 -0
- qodalis_cli_aws/processors/__init__.py +0 -0
- qodalis_cli_aws/processors/aws_command_processor.py +297 -0
- qodalis_cli_aws/processors/cloudwatch_processor.py +237 -0
- qodalis_cli_aws/processors/dynamodb_processor.py +274 -0
- qodalis_cli_aws/processors/ec2_processor.py +405 -0
- qodalis_cli_aws/processors/ecs_processor.py +236 -0
- qodalis_cli_aws/processors/iam_processor.py +208 -0
- qodalis_cli_aws/processors/lambda_processor.py +247 -0
- qodalis_cli_aws/processors/s3_processor.py +446 -0
- qodalis_cli_aws/processors/sns_processor.py +205 -0
- qodalis_cli_aws/processors/sqs_processor.py +250 -0
- qodalis_cli_aws/services/__init__.py +4 -0
- qodalis_cli_aws/services/aws_config_service.py +81 -0
- qodalis_cli_aws/services/aws_credential_manager.py +59 -0
- qodalis_cli_aws/utils/__init__.py +0 -0
- qodalis_cli_aws/utils/output_helpers.py +170 -0
- qodalis_cli_server_plugin_aws-1.0.0b13.dist-info/METADATA +25 -0
- qodalis_cli_server_plugin_aws-1.0.0b13.dist-info/RECORD +21 -0
- qodalis_cli_server_plugin_aws-1.0.0b13.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from qodalis_cli_server_abstractions import (
|
|
8
|
+
get_positional_text,
|
|
9
|
+
CliCommandParameterDescriptor,
|
|
10
|
+
CliCommandProcessor,
|
|
11
|
+
CliProcessCommand,
|
|
12
|
+
ICliCommandParameterDescriptor,
|
|
13
|
+
ICliCommandProcessor,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ..services.aws_credential_manager import AwsCredentialManager
|
|
17
|
+
from ..utils.output_helpers import (
|
|
18
|
+
build_response,
|
|
19
|
+
build_error_response,
|
|
20
|
+
format_as_table,
|
|
21
|
+
apply_output_format,
|
|
22
|
+
CliServerTextOutput,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _EcsClustersProcessor(CliCommandProcessor):
|
|
27
|
+
"""Lists all ECS clusters with status and task counts."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
self._credential_manager = credential_manager
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def command(self) -> str:
|
|
35
|
+
return "clusters"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def description(self) -> str:
|
|
39
|
+
return "List ECS clusters"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
43
|
+
return [
|
|
44
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
45
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
52
|
+
"""Fetches cluster ARNs and describes each cluster's status."""
|
|
53
|
+
region = command.args.get("region")
|
|
54
|
+
client = self._credential_manager.get_client("ecs", region=str(region) if region else None)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
list_response = client.list_clusters()
|
|
58
|
+
cluster_arns = list_response.get("clusterArns", [])
|
|
59
|
+
|
|
60
|
+
if not cluster_arns:
|
|
61
|
+
return build_response([CliServerTextOutput(value="No ECS clusters found.", style="warning")])
|
|
62
|
+
|
|
63
|
+
describe_response = client.describe_clusters(clusters=cluster_arns)
|
|
64
|
+
clusters = describe_response.get("clusters", [])
|
|
65
|
+
|
|
66
|
+
rows = [
|
|
67
|
+
[
|
|
68
|
+
c.get("clusterName", "(unknown)"),
|
|
69
|
+
c.get("status", "(unknown)"),
|
|
70
|
+
str(c.get("runningTasksCount", 0)),
|
|
71
|
+
str(c.get("pendingTasksCount", 0)),
|
|
72
|
+
]
|
|
73
|
+
for c in clusters
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
table_output = format_as_table(["Cluster Name", "Status", "Running Tasks", "Pending Tasks"], rows)
|
|
77
|
+
return build_response([apply_output_format(command, table_output, clusters)])
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
return build_error_response(f"Failed to list ECS clusters: {exc}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _EcsServicesProcessor(CliCommandProcessor):
|
|
83
|
+
"""Lists ECS services in a specified cluster."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
86
|
+
super().__init__()
|
|
87
|
+
self._credential_manager = credential_manager
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def command(self) -> str:
|
|
91
|
+
return "services"
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def description(self) -> str:
|
|
95
|
+
return "List ECS services in a cluster"
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def value_required(self) -> bool | None:
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
103
|
+
return [
|
|
104
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
105
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
109
|
+
return ""
|
|
110
|
+
|
|
111
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
112
|
+
"""Fetches and describes all services in the specified ECS cluster."""
|
|
113
|
+
cluster = get_positional_text(command)
|
|
114
|
+
if not cluster:
|
|
115
|
+
return build_error_response("Cluster name or ARN is required. Usage: ecs services <cluster>")
|
|
116
|
+
|
|
117
|
+
region = command.args.get("region")
|
|
118
|
+
client = self._credential_manager.get_client("ecs", region=str(region) if region else None)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
list_response = client.list_services(cluster=cluster)
|
|
122
|
+
service_arns = list_response.get("serviceArns", [])
|
|
123
|
+
|
|
124
|
+
if not service_arns:
|
|
125
|
+
return build_response([CliServerTextOutput(value=f'No ECS services found in cluster "{cluster}".', style="warning")])
|
|
126
|
+
|
|
127
|
+
describe_response = client.describe_services(cluster=cluster, services=service_arns)
|
|
128
|
+
services = describe_response.get("services", [])
|
|
129
|
+
|
|
130
|
+
rows = [
|
|
131
|
+
[
|
|
132
|
+
s.get("serviceName", "(unknown)"),
|
|
133
|
+
s.get("status", "(unknown)"),
|
|
134
|
+
str(s.get("desiredCount", 0)),
|
|
135
|
+
str(s.get("runningCount", 0)),
|
|
136
|
+
str(s.get("pendingCount", 0)),
|
|
137
|
+
]
|
|
138
|
+
for s in services
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
table_output = format_as_table(["Service Name", "Status", "Desired", "Running", "Pending"], rows)
|
|
142
|
+
return build_response([apply_output_format(command, table_output, services)])
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
return build_error_response(f"Failed to list ECS services: {exc}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class _EcsTasksProcessor(CliCommandProcessor):
|
|
148
|
+
"""Lists ECS tasks in a specified cluster."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
151
|
+
super().__init__()
|
|
152
|
+
self._credential_manager = credential_manager
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def command(self) -> str:
|
|
156
|
+
return "tasks"
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def description(self) -> str:
|
|
160
|
+
return "List ECS tasks in a cluster"
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def value_required(self) -> bool | None:
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
168
|
+
return [
|
|
169
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
170
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
174
|
+
return ""
|
|
175
|
+
|
|
176
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
177
|
+
"""Fetches and describes all tasks running in the specified ECS cluster."""
|
|
178
|
+
cluster = get_positional_text(command)
|
|
179
|
+
if not cluster:
|
|
180
|
+
return build_error_response("Cluster name or ARN is required. Usage: ecs tasks <cluster>")
|
|
181
|
+
|
|
182
|
+
region = command.args.get("region")
|
|
183
|
+
client = self._credential_manager.get_client("ecs", region=str(region) if region else None)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
list_response = client.list_tasks(cluster=cluster)
|
|
187
|
+
task_arns = list_response.get("taskArns", [])
|
|
188
|
+
|
|
189
|
+
if not task_arns:
|
|
190
|
+
return build_response([CliServerTextOutput(value=f'No ECS tasks found in cluster "{cluster}".', style="warning")])
|
|
191
|
+
|
|
192
|
+
describe_response = client.describe_tasks(cluster=cluster, tasks=task_arns)
|
|
193
|
+
tasks = describe_response.get("tasks", [])
|
|
194
|
+
|
|
195
|
+
rows = []
|
|
196
|
+
for task in tasks:
|
|
197
|
+
task_arn = task.get("taskArn", "")
|
|
198
|
+
task_id = task_arn.split("/")[-1] if task_arn else "(unknown)"
|
|
199
|
+
task_def_arn = task.get("taskDefinitionArn", "")
|
|
200
|
+
task_def = task_def_arn.split("/")[-1] if task_def_arn else "(unknown)"
|
|
201
|
+
started_at = task.get("startedAt")
|
|
202
|
+
started_str = started_at.strftime("%Y-%m-%d %H:%M:%S") if started_at else "(not started)"
|
|
203
|
+
rows.append([task_id, task.get("lastStatus", "(unknown)"), task_def, started_str])
|
|
204
|
+
|
|
205
|
+
table_output = format_as_table(["Task ID", "Status", "Definition", "Started"], rows)
|
|
206
|
+
return build_response([apply_output_format(command, table_output, tasks)])
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
return build_error_response(f"Failed to list ECS tasks: {exc}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class AwsEcsProcessor(CliCommandProcessor):
|
|
212
|
+
"""Parent processor for ECS sub-commands (clusters, services, tasks)."""
|
|
213
|
+
|
|
214
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
215
|
+
super().__init__()
|
|
216
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
217
|
+
_EcsClustersProcessor(credential_manager),
|
|
218
|
+
_EcsServicesProcessor(credential_manager),
|
|
219
|
+
_EcsTasksProcessor(credential_manager),
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def command(self) -> str:
|
|
224
|
+
return "ecs"
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def description(self) -> str:
|
|
228
|
+
return "AWS ECS operations -- clusters, services, tasks"
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
232
|
+
"""Returns sub-processors for ECS operations."""
|
|
233
|
+
return self._sub_processors
|
|
234
|
+
|
|
235
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
236
|
+
return ""
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from qodalis_cli_server_abstractions import (
|
|
8
|
+
CliCommandParameterDescriptor,
|
|
9
|
+
CliCommandProcessor,
|
|
10
|
+
CliProcessCommand,
|
|
11
|
+
ICliCommandParameterDescriptor,
|
|
12
|
+
ICliCommandProcessor,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from ..services.aws_credential_manager import AwsCredentialManager
|
|
16
|
+
from ..utils.output_helpers import (
|
|
17
|
+
build_response,
|
|
18
|
+
build_error_response,
|
|
19
|
+
format_as_table,
|
|
20
|
+
apply_output_format,
|
|
21
|
+
CliServerTextOutput,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _IamUsersProcessor(CliCommandProcessor):
|
|
26
|
+
"""Lists all IAM users."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._credential_manager = credential_manager
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def command(self) -> str:
|
|
34
|
+
return "users"
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def description(self) -> str:
|
|
38
|
+
return "List IAM users"
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
42
|
+
return [
|
|
43
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
44
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
48
|
+
return ""
|
|
49
|
+
|
|
50
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
51
|
+
"""Fetches and returns all IAM users as a table."""
|
|
52
|
+
region = command.args.get("region")
|
|
53
|
+
client = self._credential_manager.get_client("iam", region=str(region) if region else None)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
response = client.list_users()
|
|
57
|
+
users = response.get("Users", [])
|
|
58
|
+
|
|
59
|
+
if not users:
|
|
60
|
+
return build_response([CliServerTextOutput(value="No IAM users found.", style="warning")])
|
|
61
|
+
|
|
62
|
+
rows = [
|
|
63
|
+
[
|
|
64
|
+
user.get("UserName", "(unknown)"),
|
|
65
|
+
user.get("UserId", "(unknown)"),
|
|
66
|
+
user.get("Arn", "(unknown)"),
|
|
67
|
+
user["CreateDate"].strftime("%Y-%m-%d") if user.get("CreateDate") else "(unknown)",
|
|
68
|
+
]
|
|
69
|
+
for user in users
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
table_output = format_as_table(["UserName", "UserId", "Arn", "CreateDate"], rows)
|
|
73
|
+
return build_response([apply_output_format(command, table_output, users)])
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
return build_error_response(f"Failed to list IAM users: {exc}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _IamRolesProcessor(CliCommandProcessor):
|
|
79
|
+
"""Lists all IAM roles."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
82
|
+
super().__init__()
|
|
83
|
+
self._credential_manager = credential_manager
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def command(self) -> str:
|
|
87
|
+
return "roles"
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def description(self) -> str:
|
|
91
|
+
return "List IAM roles"
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
95
|
+
return [
|
|
96
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
97
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
104
|
+
"""Fetches and returns all IAM roles as a table."""
|
|
105
|
+
region = command.args.get("region")
|
|
106
|
+
client = self._credential_manager.get_client("iam", region=str(region) if region else None)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
response = client.list_roles()
|
|
110
|
+
roles = response.get("Roles", [])
|
|
111
|
+
|
|
112
|
+
if not roles:
|
|
113
|
+
return build_response([CliServerTextOutput(value="No IAM roles found.", style="warning")])
|
|
114
|
+
|
|
115
|
+
rows = [
|
|
116
|
+
[
|
|
117
|
+
role.get("RoleName", "(unknown)"),
|
|
118
|
+
role.get("RoleId", "(unknown)"),
|
|
119
|
+
role.get("Arn", "(unknown)"),
|
|
120
|
+
role["CreateDate"].strftime("%Y-%m-%d") if role.get("CreateDate") else "(unknown)",
|
|
121
|
+
]
|
|
122
|
+
for role in roles
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
table_output = format_as_table(["RoleName", "RoleId", "Arn", "CreateDate"], rows)
|
|
126
|
+
return build_response([apply_output_format(command, table_output, roles)])
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
return build_error_response(f"Failed to list IAM roles: {exc}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _IamPoliciesProcessor(CliCommandProcessor):
|
|
132
|
+
"""Lists customer-managed IAM policies."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
135
|
+
super().__init__()
|
|
136
|
+
self._credential_manager = credential_manager
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def command(self) -> str:
|
|
140
|
+
return "policies"
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def description(self) -> str:
|
|
144
|
+
return "List IAM policies (local/customer-managed)"
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
148
|
+
return [
|
|
149
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
150
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
154
|
+
return ""
|
|
155
|
+
|
|
156
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
157
|
+
"""Fetches and returns customer-managed IAM policies as a table."""
|
|
158
|
+
region = command.args.get("region")
|
|
159
|
+
client = self._credential_manager.get_client("iam", region=str(region) if region else None)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
response = client.list_policies(Scope="Local")
|
|
163
|
+
policies = response.get("Policies", [])
|
|
164
|
+
|
|
165
|
+
if not policies:
|
|
166
|
+
return build_response([CliServerTextOutput(value="No IAM policies found.", style="warning")])
|
|
167
|
+
|
|
168
|
+
rows = [
|
|
169
|
+
[
|
|
170
|
+
policy.get("PolicyName", "(unknown)"),
|
|
171
|
+
policy.get("Arn", "(unknown)"),
|
|
172
|
+
str(policy.get("AttachmentCount", 0)),
|
|
173
|
+
]
|
|
174
|
+
for policy in policies
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
table_output = format_as_table(["PolicyName", "Arn", "AttachmentCount"], rows)
|
|
178
|
+
return build_response([apply_output_format(command, table_output, policies)])
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
return build_error_response(f"Failed to list IAM policies: {exc}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class AwsIamProcessor(CliCommandProcessor):
|
|
184
|
+
"""Parent processor for IAM sub-commands (users, roles, policies)."""
|
|
185
|
+
|
|
186
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
187
|
+
super().__init__()
|
|
188
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
189
|
+
_IamUsersProcessor(credential_manager),
|
|
190
|
+
_IamRolesProcessor(credential_manager),
|
|
191
|
+
_IamPoliciesProcessor(credential_manager),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def command(self) -> str:
|
|
196
|
+
return "iam"
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def description(self) -> str:
|
|
200
|
+
return "AWS IAM operations -- users, roles, policies"
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
204
|
+
"""Returns sub-processors for IAM operations."""
|
|
205
|
+
return self._sub_processors
|
|
206
|
+
|
|
207
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
208
|
+
return ""
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from qodalis_cli_server_abstractions import (
|
|
9
|
+
get_positional_text,
|
|
10
|
+
CliCommandParameterDescriptor,
|
|
11
|
+
CliCommandProcessor,
|
|
12
|
+
CliProcessCommand,
|
|
13
|
+
ICliCommandParameterDescriptor,
|
|
14
|
+
ICliCommandProcessor,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from ..services.aws_credential_manager import AwsCredentialManager
|
|
18
|
+
from ..utils.output_helpers import (
|
|
19
|
+
build_response,
|
|
20
|
+
build_error_response,
|
|
21
|
+
format_as_table,
|
|
22
|
+
format_as_json,
|
|
23
|
+
apply_output_format,
|
|
24
|
+
CliServerTextOutput,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _LambdaListProcessor(CliCommandProcessor):
|
|
29
|
+
"""Lists all Lambda functions with runtime and memory details."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self._credential_manager = credential_manager
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def command(self) -> str:
|
|
37
|
+
return "list"
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def description(self) -> str:
|
|
41
|
+
return "List Lambda functions"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
45
|
+
return [
|
|
46
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
47
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
54
|
+
"""Fetches and returns all Lambda functions as a table."""
|
|
55
|
+
region = command.args.get("region")
|
|
56
|
+
client = self._credential_manager.get_client("lambda", region=str(region) if region else None)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
response = client.list_functions()
|
|
60
|
+
functions = response.get("Functions", [])
|
|
61
|
+
|
|
62
|
+
if not functions:
|
|
63
|
+
return build_response([CliServerTextOutput(value="No Lambda functions found.", style="warning")])
|
|
64
|
+
|
|
65
|
+
rows = [
|
|
66
|
+
[
|
|
67
|
+
fn.get("FunctionName", "(unknown)"),
|
|
68
|
+
fn.get("Runtime", "(unknown)"),
|
|
69
|
+
str(fn.get("MemorySize", "(unknown)")),
|
|
70
|
+
fn.get("LastModified", "(unknown)"),
|
|
71
|
+
]
|
|
72
|
+
for fn in functions
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
table_output = format_as_table(["Name", "Runtime", "Memory (MB)", "Last Modified"], rows)
|
|
76
|
+
return build_response([apply_output_format(command, table_output, functions)])
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
return build_error_response(f"Failed to list Lambda functions: {exc}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _LambdaInvokeProcessor(CliCommandProcessor):
|
|
82
|
+
"""Invokes a Lambda function and returns the result."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
85
|
+
super().__init__()
|
|
86
|
+
self._credential_manager = credential_manager
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def command(self) -> str:
|
|
90
|
+
return "invoke"
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def description(self) -> str:
|
|
94
|
+
return "Invoke a Lambda function"
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def value_required(self) -> bool | None:
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
102
|
+
return [
|
|
103
|
+
CliCommandParameterDescriptor(name="payload", description="JSON payload to send to the function", required=False, type="string", aliases=["-p"]),
|
|
104
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
108
|
+
return ""
|
|
109
|
+
|
|
110
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
111
|
+
"""Invokes the specified Lambda function with an optional JSON payload."""
|
|
112
|
+
function_name = get_positional_text(command)
|
|
113
|
+
if not function_name:
|
|
114
|
+
return build_error_response("Function name is required. Usage: lambda invoke <function-name>")
|
|
115
|
+
|
|
116
|
+
region = command.args.get("region")
|
|
117
|
+
client = self._credential_manager.get_client("lambda", region=str(region) if region else None)
|
|
118
|
+
|
|
119
|
+
payload_str = command.args.get("payload")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
params: dict[str, Any] = {"FunctionName": function_name}
|
|
123
|
+
if payload_str:
|
|
124
|
+
params["Payload"] = str(payload_str).encode("utf-8")
|
|
125
|
+
|
|
126
|
+
response = client.invoke(**params)
|
|
127
|
+
|
|
128
|
+
if response.get("FunctionError"):
|
|
129
|
+
error_body = response["Payload"].read().decode("utf-8") if response.get("Payload") else "(no details)"
|
|
130
|
+
return build_error_response(f"Function error ({response['FunctionError']}): {error_body}")
|
|
131
|
+
|
|
132
|
+
result_body = response["Payload"].read().decode("utf-8") if response.get("Payload") else "null"
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
parsed = json.loads(result_body)
|
|
136
|
+
except (json.JSONDecodeError, ValueError):
|
|
137
|
+
parsed = result_body
|
|
138
|
+
|
|
139
|
+
return build_response([
|
|
140
|
+
format_as_json({
|
|
141
|
+
"StatusCode": response.get("StatusCode"),
|
|
142
|
+
"ExecutedVersion": response.get("ExecutedVersion", "(unknown)"),
|
|
143
|
+
"Result": parsed,
|
|
144
|
+
}),
|
|
145
|
+
])
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
return build_error_response(f"Failed to invoke function: {exc}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class _LambdaLogsProcessor(CliCommandProcessor):
|
|
151
|
+
"""Fetches recent CloudWatch logs for a Lambda function."""
|
|
152
|
+
|
|
153
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
154
|
+
super().__init__()
|
|
155
|
+
self._credential_manager = credential_manager
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def command(self) -> str:
|
|
159
|
+
return "logs"
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def description(self) -> str:
|
|
163
|
+
return "View recent logs for a Lambda function"
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def value_required(self) -> bool | None:
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
171
|
+
return [
|
|
172
|
+
CliCommandParameterDescriptor(name="limit", description="Maximum number of log events (default: 50)", required=False, type="number", aliases=["-l"], default_value="50"),
|
|
173
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
177
|
+
return ""
|
|
178
|
+
|
|
179
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
180
|
+
"""Queries the ``/aws/lambda/<name>`` log group for recent events."""
|
|
181
|
+
function_name = get_positional_text(command)
|
|
182
|
+
if not function_name:
|
|
183
|
+
return build_error_response("Function name is required. Usage: lambda logs <function-name>")
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
limit = int(command.args.get("limit", 50))
|
|
187
|
+
except (ValueError, TypeError):
|
|
188
|
+
return build_error_response("--limit must be a positive number.")
|
|
189
|
+
|
|
190
|
+
if limit <= 0:
|
|
191
|
+
return build_error_response("--limit must be a positive number.")
|
|
192
|
+
|
|
193
|
+
region = command.args.get("region")
|
|
194
|
+
client = self._credential_manager.get_client("logs", region=str(region) if region else None)
|
|
195
|
+
|
|
196
|
+
log_group_name = f"/aws/lambda/{function_name}"
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
response = client.filter_log_events(logGroupName=log_group_name, limit=limit)
|
|
200
|
+
events = response.get("events", [])
|
|
201
|
+
|
|
202
|
+
if not events:
|
|
203
|
+
return build_response([CliServerTextOutput(value=f"No log events found for {log_group_name}.", style="warning")])
|
|
204
|
+
|
|
205
|
+
from datetime import datetime, timezone
|
|
206
|
+
|
|
207
|
+
lines = []
|
|
208
|
+
for event in events:
|
|
209
|
+
ts = event.get("timestamp")
|
|
210
|
+
if ts:
|
|
211
|
+
timestamp = datetime.fromtimestamp(ts / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
212
|
+
else:
|
|
213
|
+
timestamp = "(unknown)"
|
|
214
|
+
message = (event.get("message") or "").rstrip()
|
|
215
|
+
lines.append(f"{timestamp} {message}")
|
|
216
|
+
|
|
217
|
+
return build_response([CliServerTextOutput(value="\n".join(lines))])
|
|
218
|
+
except Exception as exc:
|
|
219
|
+
return build_error_response(f"Failed to fetch logs: {exc}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class AwsLambdaProcessor(CliCommandProcessor):
|
|
223
|
+
"""Parent processor for Lambda sub-commands (list, invoke, logs)."""
|
|
224
|
+
|
|
225
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
226
|
+
super().__init__()
|
|
227
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
228
|
+
_LambdaListProcessor(credential_manager),
|
|
229
|
+
_LambdaInvokeProcessor(credential_manager),
|
|
230
|
+
_LambdaLogsProcessor(credential_manager),
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def command(self) -> str:
|
|
235
|
+
return "lambda"
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def description(self) -> str:
|
|
239
|
+
return "AWS Lambda operations -- list, invoke, and view logs"
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
243
|
+
"""Returns sub-processors for Lambda operations."""
|
|
244
|
+
return self._sub_processors
|
|
245
|
+
|
|
246
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
247
|
+
return ""
|