stackport 0.3.8__tar.gz → 0.3.9__tar.gz
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.
- {stackport-0.3.8/stackport.egg-info → stackport-0.3.9}/PKG-INFO +1 -1
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/logs.py +94 -1
- stackport-0.3.9/backend/schemas/logs.py +21 -0
- {stackport-0.3.8 → stackport-0.3.9}/pyproject.toml +1 -1
- {stackport-0.3.8 → stackport-0.3.9/stackport.egg-info}/PKG-INFO +1 -1
- {stackport-0.3.8 → stackport-0.3.9}/stackport.egg-info/SOURCES.txt +5 -4
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_logs_routes.py +159 -0
- stackport-0.3.8/ui/dist/assets/StateMachineGraph-CmFIj15P.js → stackport-0.3.9/ui/dist/assets/StateMachineGraph-BGBIYpUV.js +1 -1
- stackport-0.3.9/ui/dist/assets/index-iwgw2OEV.js +652 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/index.html +1 -1
- stackport-0.3.8/ui/dist/assets/index-Dvr-Ft0R.js +0 -652
- {stackport-0.3.8 → stackport-0.3.9}/LICENSE +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/MANIFEST.in +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/README.md +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/__init__.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/aws_client.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/cache.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/cli.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/config.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/endpoint_store.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/main.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/__init__.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/common.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/dynamodb.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/ec2.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/endpoints.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/iam.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/lambda_svc.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/rds.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/resources.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/s3.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/secretsmanager.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/sqs.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/stats.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/stepfunctions.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/routes/tags.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/__init__.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/dynamodb.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/endpoints.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/lambda_svc.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/s3.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/secretsmanager.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/sqs.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/stepfunctions.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/schemas/tags.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/backend/websocket.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/setup.cfg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/stackport.egg-info/requires.txt +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/stackport.egg-info/top_level.txt +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_cache.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_cli.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_client.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_config.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_dynamodb_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_ec2_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_endpoint_store.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_endpoints.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_iam_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_lambda_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_multi_endpoint.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_rds_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_readonly_middleware.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_registries.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_s3_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_s3_upload_limit_env.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_secretsmanager_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_sqs_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_tags_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/tests/test_websocket.py +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/assets/index-BZr5kWvs.css +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/acm.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/apigateway.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/appsync.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/athena.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/cloudformation.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/cloudfront.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/cognito-idp.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/dynamodb.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/ec2.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/ecr.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/ecs.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/elasticache.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/events.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/firehose.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/glue.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/iam.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/kinesis.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/kms.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/lambda.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/logs.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/monitoring.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/rds.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/route53.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/s3.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/secretsmanager.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/ses.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/sns.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/sqs.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/ssm.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/stepfunctions.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/aws-icons/wafv2.svg +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/favicon.png +0 -0
- {stackport-0.3.8 → stackport-0.3.9}/ui/dist/favicon.svg +0 -0
|
@@ -2,16 +2,19 @@ import logging
|
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
-
from fastapi import APIRouter, Depends, Query
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
6
6
|
|
|
7
7
|
from backend.aws_client import get_client
|
|
8
8
|
from backend.cache import cache
|
|
9
9
|
from backend.routes.common import EndpointInfo, get_endpoint_info
|
|
10
|
+
from backend.schemas.logs import CreateLogGroupBody, SetRetentionBody
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
13
14
|
router = APIRouter()
|
|
14
15
|
|
|
16
|
+
VALID_RETENTION_DAYS = {1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653}
|
|
17
|
+
|
|
15
18
|
|
|
16
19
|
def _epoch_millis_to_iso(timestamp: Optional[int]) -> Optional[str]:
|
|
17
20
|
"""Convert epoch milliseconds to ISO string."""
|
|
@@ -218,3 +221,93 @@ def get_log_events(
|
|
|
218
221
|
"events": events,
|
|
219
222
|
"next_token": response.get("nextForwardToken"),
|
|
220
223
|
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@router.post("/groups", status_code=201)
|
|
227
|
+
def create_log_group(body: CreateLogGroupBody, ep: EndpointInfo = Depends(get_endpoint_info)):
|
|
228
|
+
"""Create a new CloudWatch log group."""
|
|
229
|
+
if body.retention_in_days is not None and body.retention_in_days not in VALID_RETENTION_DAYS:
|
|
230
|
+
raise HTTPException(
|
|
231
|
+
status_code=400,
|
|
232
|
+
detail=f"retention_in_days must be one of {sorted(VALID_RETENTION_DAYS)}",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
logs = get_client("logs", **ep.client_kwargs())
|
|
236
|
+
try:
|
|
237
|
+
kwargs: dict = {"logGroupName": body.name}
|
|
238
|
+
if body.tags:
|
|
239
|
+
kwargs["tags"] = body.tags
|
|
240
|
+
logs.create_log_group(**kwargs)
|
|
241
|
+
|
|
242
|
+
if body.retention_in_days is not None:
|
|
243
|
+
logs.put_retention_policy(
|
|
244
|
+
logGroupName=body.name, retentionInDays=body.retention_in_days
|
|
245
|
+
)
|
|
246
|
+
except logs.exceptions.ResourceAlreadyExistsException:
|
|
247
|
+
raise HTTPException(status_code=409, detail=f"Log group '{body.name}' already exists")
|
|
248
|
+
except Exception as e:
|
|
249
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
250
|
+
|
|
251
|
+
cache.delete_by_prefix(f"{ep.url}:logs:groups:")
|
|
252
|
+
return {"name": body.name, "retention_in_days": body.retention_in_days}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@router.delete("/groups/{name:path}/streams/{stream:path}")
|
|
256
|
+
def delete_log_stream(name: str, stream: str, ep: EndpointInfo = Depends(get_endpoint_info)):
|
|
257
|
+
"""Delete a log stream from a log group.
|
|
258
|
+
|
|
259
|
+
Registered before delete_log_group: {name:path} matches slashes greedily,
|
|
260
|
+
so if the bare /groups/{name:path} DELETE route were checked first it would
|
|
261
|
+
swallow this whole path (including "/streams/{stream}") as the group name
|
|
262
|
+
and never reach this handler. The more specific route has to come first.
|
|
263
|
+
"""
|
|
264
|
+
logs = get_client("logs", **ep.client_kwargs())
|
|
265
|
+
try:
|
|
266
|
+
logs.delete_log_stream(logGroupName=name, logStreamName=stream)
|
|
267
|
+
except logs.exceptions.ResourceNotFoundException:
|
|
268
|
+
raise HTTPException(status_code=404, detail=f"Log stream '{stream}' not found")
|
|
269
|
+
except Exception as e:
|
|
270
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
271
|
+
|
|
272
|
+
cache.delete_by_prefix(f"{ep.url}:logs:streams:{name}:")
|
|
273
|
+
return {"success": True, "message": f"Log stream '{stream}' deleted"}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@router.put("/groups/{name:path}/retention")
|
|
277
|
+
def set_log_group_retention(name: str, body: SetRetentionBody, ep: EndpointInfo = Depends(get_endpoint_info)):
|
|
278
|
+
"""Set or remove a log group's retention policy."""
|
|
279
|
+
if body.retention_in_days is not None and body.retention_in_days not in VALID_RETENTION_DAYS:
|
|
280
|
+
raise HTTPException(
|
|
281
|
+
status_code=400,
|
|
282
|
+
detail=f"retention_in_days must be one of {sorted(VALID_RETENTION_DAYS)}",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
logs = get_client("logs", **ep.client_kwargs())
|
|
286
|
+
try:
|
|
287
|
+
if body.retention_in_days is None:
|
|
288
|
+
logs.delete_retention_policy(logGroupName=name)
|
|
289
|
+
else:
|
|
290
|
+
logs.put_retention_policy(logGroupName=name, retentionInDays=body.retention_in_days)
|
|
291
|
+
except logs.exceptions.ResourceNotFoundException:
|
|
292
|
+
raise HTTPException(status_code=404, detail=f"Log group '{name}' not found")
|
|
293
|
+
except Exception as e:
|
|
294
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
295
|
+
|
|
296
|
+
cache.delete_by_prefix(f"{ep.url}:logs:groups:")
|
|
297
|
+
return {"name": name, "retention_in_days": body.retention_in_days}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@router.delete("/groups/{name:path}")
|
|
301
|
+
def delete_log_group(name: str, ep: EndpointInfo = Depends(get_endpoint_info)):
|
|
302
|
+
"""Delete a CloudWatch log group."""
|
|
303
|
+
logs = get_client("logs", **ep.client_kwargs())
|
|
304
|
+
try:
|
|
305
|
+
logs.delete_log_group(logGroupName=name)
|
|
306
|
+
except logs.exceptions.ResourceNotFoundException:
|
|
307
|
+
raise HTTPException(status_code=404, detail=f"Log group '{name}' not found")
|
|
308
|
+
except Exception as e:
|
|
309
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
310
|
+
|
|
311
|
+
cache.delete_by_prefix(f"{ep.url}:logs:groups:")
|
|
312
|
+
cache.delete_by_prefix(f"{ep.url}:logs:streams:{name}:")
|
|
313
|
+
return {"success": True, "message": f"Log group '{name}' deleted"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Pydantic schemas for CloudWatch Logs API requests."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CreateLogGroupBody(BaseModel):
|
|
7
|
+
"""Request body for creating a new log group."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
10
|
+
|
|
11
|
+
name: str = Field(..., description="Log group name")
|
|
12
|
+
retention_in_days: int | None = Field(None, alias="retentionInDays", description="Retention period in days")
|
|
13
|
+
tags: dict[str, str] = Field(default_factory=dict, description="Resource tags")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SetRetentionBody(BaseModel):
|
|
17
|
+
"""Request body for setting a log group's retention policy."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
20
|
+
|
|
21
|
+
retention_in_days: int | None = Field(None, alias="retentionInDays", description="Retention period in days, or null to never expire")
|
|
@@ -13,9 +13,9 @@ backend/websocket.py
|
|
|
13
13
|
backend/../ui/dist/favicon.png
|
|
14
14
|
backend/../ui/dist/favicon.svg
|
|
15
15
|
backend/../ui/dist/index.html
|
|
16
|
-
backend/../ui/dist/assets/StateMachineGraph-
|
|
16
|
+
backend/../ui/dist/assets/StateMachineGraph-BGBIYpUV.js
|
|
17
17
|
backend/../ui/dist/assets/index-BZr5kWvs.css
|
|
18
|
-
backend/../ui/dist/assets/index-
|
|
18
|
+
backend/../ui/dist/assets/index-iwgw2OEV.js
|
|
19
19
|
backend/../ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
|
|
20
20
|
backend/../ui/dist/aws-icons/acm.svg
|
|
21
21
|
backend/../ui/dist/aws-icons/apigateway.svg
|
|
@@ -71,6 +71,7 @@ backend/schemas/__init__.py
|
|
|
71
71
|
backend/schemas/dynamodb.py
|
|
72
72
|
backend/schemas/endpoints.py
|
|
73
73
|
backend/schemas/lambda_svc.py
|
|
74
|
+
backend/schemas/logs.py
|
|
74
75
|
backend/schemas/s3.py
|
|
75
76
|
backend/schemas/secretsmanager.py
|
|
76
77
|
backend/schemas/sqs.py
|
|
@@ -107,9 +108,9 @@ tests/test_websocket.py
|
|
|
107
108
|
ui/dist/favicon.png
|
|
108
109
|
ui/dist/favicon.svg
|
|
109
110
|
ui/dist/index.html
|
|
110
|
-
ui/dist/assets/StateMachineGraph-
|
|
111
|
+
ui/dist/assets/StateMachineGraph-BGBIYpUV.js
|
|
111
112
|
ui/dist/assets/index-BZr5kWvs.css
|
|
112
|
-
ui/dist/assets/index-
|
|
113
|
+
ui/dist/assets/index-iwgw2OEV.js
|
|
113
114
|
ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
|
|
114
115
|
ui/dist/aws-icons/acm.svg
|
|
115
116
|
ui/dist/aws-icons/apigateway.svg
|
|
@@ -272,3 +272,162 @@ class TestGetLogEvents:
|
|
|
272
272
|
assert resp.status_code == 200
|
|
273
273
|
data = resp.json()
|
|
274
274
|
assert data["events"] == []
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TestCreateLogGroup:
|
|
278
|
+
@patch("backend.routes.logs.cache")
|
|
279
|
+
@patch("backend.routes.logs.get_client")
|
|
280
|
+
def test_create_log_group(self, mock_get_client, mock_cache):
|
|
281
|
+
mock_logs = MagicMock()
|
|
282
|
+
mock_get_client.return_value = mock_logs
|
|
283
|
+
|
|
284
|
+
resp = client.post("/api/logs/groups", json={"name": "/aws/lambda/new-fn"})
|
|
285
|
+
assert resp.status_code == 201
|
|
286
|
+
data = resp.json()
|
|
287
|
+
assert data["name"] == "/aws/lambda/new-fn"
|
|
288
|
+
assert data["retention_in_days"] is None
|
|
289
|
+
mock_logs.create_log_group.assert_called_once_with(logGroupName="/aws/lambda/new-fn")
|
|
290
|
+
mock_logs.put_retention_policy.assert_not_called()
|
|
291
|
+
|
|
292
|
+
@patch("backend.routes.logs.cache")
|
|
293
|
+
@patch("backend.routes.logs.get_client")
|
|
294
|
+
def test_create_log_group_with_retention_and_tags(self, mock_get_client, mock_cache):
|
|
295
|
+
mock_logs = MagicMock()
|
|
296
|
+
mock_get_client.return_value = mock_logs
|
|
297
|
+
|
|
298
|
+
resp = client.post(
|
|
299
|
+
"/api/logs/groups",
|
|
300
|
+
json={"name": "/aws/lambda/new-fn", "retentionInDays": 14, "tags": {"env": "prod"}},
|
|
301
|
+
)
|
|
302
|
+
assert resp.status_code == 201
|
|
303
|
+
data = resp.json()
|
|
304
|
+
assert data["retention_in_days"] == 14
|
|
305
|
+
|
|
306
|
+
mock_logs.create_log_group.assert_called_once_with(
|
|
307
|
+
logGroupName="/aws/lambda/new-fn", tags={"env": "prod"}
|
|
308
|
+
)
|
|
309
|
+
mock_logs.put_retention_policy.assert_called_once_with(
|
|
310
|
+
logGroupName="/aws/lambda/new-fn", retentionInDays=14
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
@patch("backend.routes.logs.get_client")
|
|
314
|
+
def test_create_log_group_invalid_retention(self, mock_get_client):
|
|
315
|
+
resp = client.post(
|
|
316
|
+
"/api/logs/groups", json={"name": "/aws/lambda/new-fn", "retentionInDays": 13}
|
|
317
|
+
)
|
|
318
|
+
assert resp.status_code == 400
|
|
319
|
+
|
|
320
|
+
@patch("backend.routes.logs.get_client")
|
|
321
|
+
def test_create_log_group_already_exists(self, mock_get_client):
|
|
322
|
+
mock_logs = MagicMock()
|
|
323
|
+
mock_get_client.return_value = mock_logs
|
|
324
|
+
mock_logs.exceptions.ResourceAlreadyExistsException = type(
|
|
325
|
+
"ResourceAlreadyExistsException", (Exception,), {}
|
|
326
|
+
)
|
|
327
|
+
mock_logs.create_log_group.side_effect = mock_logs.exceptions.ResourceAlreadyExistsException()
|
|
328
|
+
|
|
329
|
+
resp = client.post("/api/logs/groups", json={"name": "existing-group"})
|
|
330
|
+
assert resp.status_code == 409
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class TestDeleteLogGroup:
|
|
334
|
+
@patch("backend.routes.logs.cache")
|
|
335
|
+
@patch("backend.routes.logs.get_client")
|
|
336
|
+
def test_delete_log_group(self, mock_get_client, mock_cache):
|
|
337
|
+
mock_logs = MagicMock()
|
|
338
|
+
mock_get_client.return_value = mock_logs
|
|
339
|
+
|
|
340
|
+
resp = client.delete("/api/logs/groups/my-log-group")
|
|
341
|
+
assert resp.status_code == 200
|
|
342
|
+
assert resp.json()["success"] is True
|
|
343
|
+
mock_logs.delete_log_group.assert_called_once_with(logGroupName="my-log-group")
|
|
344
|
+
|
|
345
|
+
@patch("backend.routes.logs.get_client")
|
|
346
|
+
def test_delete_log_group_not_found(self, mock_get_client):
|
|
347
|
+
mock_logs = MagicMock()
|
|
348
|
+
mock_get_client.return_value = mock_logs
|
|
349
|
+
mock_logs.exceptions.ResourceNotFoundException = type(
|
|
350
|
+
"ResourceNotFoundException", (Exception,), {}
|
|
351
|
+
)
|
|
352
|
+
mock_logs.delete_log_group.side_effect = mock_logs.exceptions.ResourceNotFoundException()
|
|
353
|
+
|
|
354
|
+
resp = client.delete("/api/logs/groups/missing-group")
|
|
355
|
+
assert resp.status_code == 404
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class TestSetLogGroupRetention:
|
|
359
|
+
@patch("backend.routes.logs.cache")
|
|
360
|
+
@patch("backend.routes.logs.get_client")
|
|
361
|
+
def test_set_retention(self, mock_get_client, mock_cache):
|
|
362
|
+
mock_logs = MagicMock()
|
|
363
|
+
mock_get_client.return_value = mock_logs
|
|
364
|
+
|
|
365
|
+
resp = client.put(
|
|
366
|
+
"/api/logs/groups/my-log-group/retention", json={"retentionInDays": 30}
|
|
367
|
+
)
|
|
368
|
+
assert resp.status_code == 200
|
|
369
|
+
assert resp.json()["retention_in_days"] == 30
|
|
370
|
+
mock_logs.put_retention_policy.assert_called_once_with(
|
|
371
|
+
logGroupName="my-log-group", retentionInDays=30
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
@patch("backend.routes.logs.cache")
|
|
375
|
+
@patch("backend.routes.logs.get_client")
|
|
376
|
+
def test_clear_retention(self, mock_get_client, mock_cache):
|
|
377
|
+
mock_logs = MagicMock()
|
|
378
|
+
mock_get_client.return_value = mock_logs
|
|
379
|
+
|
|
380
|
+
resp = client.put(
|
|
381
|
+
"/api/logs/groups/my-log-group/retention", json={"retentionInDays": None}
|
|
382
|
+
)
|
|
383
|
+
assert resp.status_code == 200
|
|
384
|
+
assert resp.json()["retention_in_days"] is None
|
|
385
|
+
mock_logs.delete_retention_policy.assert_called_once_with(logGroupName="my-log-group")
|
|
386
|
+
|
|
387
|
+
@patch("backend.routes.logs.get_client")
|
|
388
|
+
def test_set_retention_invalid_value(self, mock_get_client):
|
|
389
|
+
resp = client.put(
|
|
390
|
+
"/api/logs/groups/my-log-group/retention", json={"retentionInDays": 999}
|
|
391
|
+
)
|
|
392
|
+
assert resp.status_code == 400
|
|
393
|
+
|
|
394
|
+
@patch("backend.routes.logs.get_client")
|
|
395
|
+
def test_set_retention_group_not_found(self, mock_get_client):
|
|
396
|
+
mock_logs = MagicMock()
|
|
397
|
+
mock_get_client.return_value = mock_logs
|
|
398
|
+
mock_logs.exceptions.ResourceNotFoundException = type(
|
|
399
|
+
"ResourceNotFoundException", (Exception,), {}
|
|
400
|
+
)
|
|
401
|
+
mock_logs.put_retention_policy.side_effect = mock_logs.exceptions.ResourceNotFoundException()
|
|
402
|
+
|
|
403
|
+
resp = client.put(
|
|
404
|
+
"/api/logs/groups/missing-group/retention", json={"retentionInDays": 30}
|
|
405
|
+
)
|
|
406
|
+
assert resp.status_code == 404
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class TestDeleteLogStream:
|
|
410
|
+
@patch("backend.routes.logs.cache")
|
|
411
|
+
@patch("backend.routes.logs.get_client")
|
|
412
|
+
def test_delete_log_stream(self, mock_get_client, mock_cache):
|
|
413
|
+
mock_logs = MagicMock()
|
|
414
|
+
mock_get_client.return_value = mock_logs
|
|
415
|
+
|
|
416
|
+
resp = client.delete("/api/logs/groups/my-log-group/streams/my-stream")
|
|
417
|
+
assert resp.status_code == 200
|
|
418
|
+
assert resp.json()["success"] is True
|
|
419
|
+
mock_logs.delete_log_stream.assert_called_once_with(
|
|
420
|
+
logGroupName="my-log-group", logStreamName="my-stream"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
@patch("backend.routes.logs.get_client")
|
|
424
|
+
def test_delete_log_stream_not_found(self, mock_get_client):
|
|
425
|
+
mock_logs = MagicMock()
|
|
426
|
+
mock_get_client.return_value = mock_logs
|
|
427
|
+
mock_logs.exceptions.ResourceNotFoundException = type(
|
|
428
|
+
"ResourceNotFoundException", (Exception,), {}
|
|
429
|
+
)
|
|
430
|
+
mock_logs.delete_log_stream.side_effect = mock_logs.exceptions.ResourceNotFoundException()
|
|
431
|
+
|
|
432
|
+
resp = client.delete("/api/logs/groups/my-log-group/streams/missing-stream")
|
|
433
|
+
assert resp.status_code == 404
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as L,C as z,a as O,b as Y,G as J,j as n,r as m,B as j}from"./index-
|
|
1
|
+
import{c as L,C as z,a as O,b as Y,G as J,j as n,r as m,B as j}from"./index-iwgw2OEV.js";import{T as B}from"./stepfunctions-graph-B6nvU4OB.js";/**
|
|
2
2
|
* @license lucide-react v1.7.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|