stackport 0.3.0__tar.gz → 0.3.2__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.0/stackport.egg-info → stackport-0.3.2}/PKG-INFO +1 -1
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/lambda_svc.py +52 -0
- stackport-0.3.2/backend/routes/secretsmanager.py +256 -0
- stackport-0.3.2/backend/schemas/lambda_svc.py +23 -0
- stackport-0.3.2/backend/schemas/secretsmanager.py +33 -0
- {stackport-0.3.0 → stackport-0.3.2}/pyproject.toml +1 -1
- {stackport-0.3.0 → stackport-0.3.2/stackport.egg-info}/PKG-INFO +1 -1
- {stackport-0.3.0 → stackport-0.3.2}/stackport.egg-info/SOURCES.txt +6 -4
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_lambda_routes.py +181 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_secretsmanager_routes.py +227 -0
- stackport-0.3.2/ui/dist/assets/index-BJ2zUhqY.css +1 -0
- stackport-0.3.2/ui/dist/assets/index-D8i-Md_b.js +639 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/index.html +2 -2
- stackport-0.3.0/backend/routes/secretsmanager.py +0 -113
- stackport-0.3.0/ui/dist/assets/index-Cda_0qu5.css +0 -1
- stackport-0.3.0/ui/dist/assets/index-tW-8e4eS.js +0 -629
- {stackport-0.3.0 → stackport-0.3.2}/LICENSE +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/MANIFEST.in +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/README.md +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/__init__.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/aws_client.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/cache.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/cli.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/config.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/endpoint_store.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/main.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/__init__.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/common.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/dynamodb.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/ec2.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/endpoints.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/iam.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/logs.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/resources.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/s3.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/sqs.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/stats.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/routes/tags.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/__init__.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/dynamodb.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/endpoints.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/s3.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/sqs.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/tags.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/backend/websocket.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/setup.cfg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/stackport.egg-info/requires.txt +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/stackport.egg-info/top_level.txt +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_cache.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_cli.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_client.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_config.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_dynamodb_routes.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_ec2_routes.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_endpoint_store.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_endpoints.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_iam_routes.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_logs_routes.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_multi_endpoint.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_readonly_middleware.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_registries.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_routes.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_s3_routes.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_s3_upload_limit_env.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_sqs_routes.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_tags_routes.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/tests/test_websocket.py +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/acm.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/apigateway.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/appsync.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/athena.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/cloudformation.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/cloudfront.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/cognito-idp.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/dynamodb.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/ec2.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/ecr.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/ecs.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/elasticache.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/events.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/firehose.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/glue.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/iam.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/kinesis.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/kms.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/lambda.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/logs.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/monitoring.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/rds.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/route53.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/s3.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/secretsmanager.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/ses.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/sns.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/sqs.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/ssm.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/stepfunctions.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/wafv2.svg +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/favicon.png +0 -0
- {stackport-0.3.0 → stackport-0.3.2}/ui/dist/favicon.svg +0 -0
|
@@ -9,6 +9,7 @@ from fastapi.responses import RedirectResponse
|
|
|
9
9
|
|
|
10
10
|
from backend.aws_client import get_client
|
|
11
11
|
from backend.routes.common import EndpointInfo, get_endpoint_info
|
|
12
|
+
from backend.schemas.lambda_svc import UpdateFunctionConfigRequest
|
|
12
13
|
|
|
13
14
|
router = APIRouter()
|
|
14
15
|
|
|
@@ -184,3 +185,54 @@ def list_versions(function_name: str, ep: EndpointInfo = Depends(get_endpoint_in
|
|
|
184
185
|
return {"versions": versions}
|
|
185
186
|
except Exception as e:
|
|
186
187
|
raise HTTPException(status_code=500, detail=str(e))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.patch("/functions/{function_name}/configuration")
|
|
191
|
+
def update_function_configuration(
|
|
192
|
+
function_name: str,
|
|
193
|
+
body: UpdateFunctionConfigRequest,
|
|
194
|
+
ep: EndpointInfo = Depends(get_endpoint_info)
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""Update Lambda function configuration (partial updates supported).
|
|
197
|
+
|
|
198
|
+
All body fields are optional — only specified fields will be updated.
|
|
199
|
+
Returns the updated function configuration.
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
client = get_client("lambda", **ep.client_kwargs())
|
|
203
|
+
|
|
204
|
+
# Build boto3 kwargs from request body, skipping None values
|
|
205
|
+
update_kwargs: dict[str, Any] = {"FunctionName": function_name}
|
|
206
|
+
|
|
207
|
+
if body.description is not None:
|
|
208
|
+
update_kwargs["Description"] = body.description
|
|
209
|
+
|
|
210
|
+
if body.handler is not None:
|
|
211
|
+
update_kwargs["Handler"] = body.handler
|
|
212
|
+
|
|
213
|
+
if body.runtime is not None:
|
|
214
|
+
update_kwargs["Runtime"] = body.runtime
|
|
215
|
+
|
|
216
|
+
if body.memory_size is not None:
|
|
217
|
+
update_kwargs["MemorySize"] = body.memory_size
|
|
218
|
+
|
|
219
|
+
if body.timeout is not None:
|
|
220
|
+
update_kwargs["Timeout"] = body.timeout
|
|
221
|
+
|
|
222
|
+
if body.environment is not None:
|
|
223
|
+
update_kwargs["Environment"] = {"Variables": body.environment}
|
|
224
|
+
|
|
225
|
+
if body.layers is not None:
|
|
226
|
+
update_kwargs["Layers"] = body.layers
|
|
227
|
+
|
|
228
|
+
# Call update_function_configuration
|
|
229
|
+
response = client.update_function_configuration(**update_kwargs)
|
|
230
|
+
|
|
231
|
+
return {"configuration": response}
|
|
232
|
+
|
|
233
|
+
except client.exceptions.ResourceNotFoundException:
|
|
234
|
+
raise HTTPException(status_code=404, detail=f"Function {function_name} not found")
|
|
235
|
+
except client.exceptions.InvalidParameterValueException as e:
|
|
236
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
237
|
+
except Exception as e:
|
|
238
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Secrets Manager service-specific routes."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
7
|
+
|
|
8
|
+
from backend.aws_client import get_client
|
|
9
|
+
from backend.routes.common import EndpointInfo, get_endpoint_info
|
|
10
|
+
from backend.schemas.secretsmanager import (
|
|
11
|
+
CreateSecretBody,
|
|
12
|
+
UpdateSecretMetadataBody,
|
|
13
|
+
UpdateSecretValueBody,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _format_date(dt) -> str | None:
|
|
20
|
+
"""Format a datetime to ISO string, or return None."""
|
|
21
|
+
if dt is None:
|
|
22
|
+
return None
|
|
23
|
+
try:
|
|
24
|
+
return dt.isoformat()
|
|
25
|
+
except Exception:
|
|
26
|
+
return str(dt)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.get("/secrets")
|
|
30
|
+
def list_secrets(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
|
|
31
|
+
"""List all secrets with metadata."""
|
|
32
|
+
try:
|
|
33
|
+
client = get_client("secretsmanager", **ep.client_kwargs())
|
|
34
|
+
paginator = client.get_paginator("list_secrets")
|
|
35
|
+
|
|
36
|
+
secrets = []
|
|
37
|
+
for page in paginator.paginate():
|
|
38
|
+
for secret in page.get("SecretList", []):
|
|
39
|
+
secrets.append(
|
|
40
|
+
{
|
|
41
|
+
"name": secret.get("Name"),
|
|
42
|
+
"arn": secret.get("ARN"),
|
|
43
|
+
"description": secret.get("Description", ""),
|
|
44
|
+
"createdDate": _format_date(secret.get("CreatedDate")),
|
|
45
|
+
"lastChangedDate": _format_date(
|
|
46
|
+
secret.get("LastChangedDate")
|
|
47
|
+
),
|
|
48
|
+
"lastAccessedDate": _format_date(
|
|
49
|
+
secret.get("LastAccessedDate")
|
|
50
|
+
),
|
|
51
|
+
"rotationEnabled": secret.get("RotationEnabled", False),
|
|
52
|
+
"tags": {
|
|
53
|
+
tag["Key"]: tag["Value"]
|
|
54
|
+
for tag in secret.get("Tags", [])
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return {"secrets": secrets}
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.get("/secrets/{secret_id:path}")
|
|
65
|
+
def get_secret_detail(secret_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
|
|
66
|
+
"""Get secret metadata and value."""
|
|
67
|
+
try:
|
|
68
|
+
client = get_client("secretsmanager", **ep.client_kwargs())
|
|
69
|
+
|
|
70
|
+
# Get metadata
|
|
71
|
+
try:
|
|
72
|
+
meta = client.describe_secret(SecretId=secret_id)
|
|
73
|
+
except client.exceptions.ResourceNotFoundException:
|
|
74
|
+
raise HTTPException(
|
|
75
|
+
status_code=404, detail=f"Secret '{secret_id}' not found"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Get value
|
|
79
|
+
secret_value = None
|
|
80
|
+
secret_binary = None
|
|
81
|
+
version_id = None
|
|
82
|
+
version_stages = None
|
|
83
|
+
try:
|
|
84
|
+
value_resp = client.get_secret_value(SecretId=secret_id)
|
|
85
|
+
secret_value = value_resp.get("SecretString")
|
|
86
|
+
raw_binary = value_resp.get("SecretBinary")
|
|
87
|
+
if raw_binary is not None:
|
|
88
|
+
secret_binary = base64.b64encode(raw_binary).decode("utf-8")
|
|
89
|
+
version_id = value_resp.get("VersionId")
|
|
90
|
+
version_stages = value_resp.get("VersionStages")
|
|
91
|
+
except Exception:
|
|
92
|
+
# Value may not be retrievable (e.g., pending deletion)
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
"name": meta.get("Name"),
|
|
97
|
+
"arn": meta.get("ARN"),
|
|
98
|
+
"description": meta.get("Description", ""),
|
|
99
|
+
"createdDate": _format_date(meta.get("CreatedDate")),
|
|
100
|
+
"lastChangedDate": _format_date(meta.get("LastChangedDate")),
|
|
101
|
+
"lastAccessedDate": _format_date(meta.get("LastAccessedDate")),
|
|
102
|
+
"rotationEnabled": meta.get("RotationEnabled", False),
|
|
103
|
+
"rotationRules": meta.get("RotationRules"),
|
|
104
|
+
"rotationLambdaARN": meta.get("RotationLambdaARN"),
|
|
105
|
+
"deletedDate": _format_date(meta.get("DeletedDate")),
|
|
106
|
+
"tags": {
|
|
107
|
+
tag["Key"]: tag["Value"]
|
|
108
|
+
for tag in meta.get("Tags", [])
|
|
109
|
+
},
|
|
110
|
+
"versionId": version_id,
|
|
111
|
+
"versionStages": version_stages,
|
|
112
|
+
"secretValue": secret_value,
|
|
113
|
+
"secretBinary": secret_binary,
|
|
114
|
+
}
|
|
115
|
+
except HTTPException:
|
|
116
|
+
raise
|
|
117
|
+
except Exception as e:
|
|
118
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@router.post("/secrets", status_code=201)
|
|
122
|
+
def create_secret(body: CreateSecretBody, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
|
|
123
|
+
"""Create a new secret."""
|
|
124
|
+
if not body.secret_string and not body.secret_binary:
|
|
125
|
+
raise HTTPException(status_code=400, detail="Must provide either secret_string or secret_binary")
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
client = get_client("secretsmanager", **ep.client_kwargs())
|
|
129
|
+
|
|
130
|
+
kwargs: dict[str, Any] = {"Name": body.name}
|
|
131
|
+
if body.description:
|
|
132
|
+
kwargs["Description"] = body.description
|
|
133
|
+
if body.secret_string:
|
|
134
|
+
kwargs["SecretString"] = body.secret_string
|
|
135
|
+
if body.secret_binary:
|
|
136
|
+
kwargs["SecretBinary"] = base64.b64decode(body.secret_binary)
|
|
137
|
+
if body.tags:
|
|
138
|
+
kwargs["Tags"] = [{"Key": k, "Value": v} for k, v in body.tags.items()]
|
|
139
|
+
|
|
140
|
+
resp = client.create_secret(**kwargs)
|
|
141
|
+
return {
|
|
142
|
+
"name": resp["Name"],
|
|
143
|
+
"arn": resp["ARN"],
|
|
144
|
+
"versionId": resp.get("VersionId"),
|
|
145
|
+
}
|
|
146
|
+
except client.exceptions.ResourceExistsException:
|
|
147
|
+
raise HTTPException(status_code=409, detail=f"Secret '{body.name}' already exists")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router.put("/secrets/{secret_id:path}/value")
|
|
153
|
+
def update_secret_value(secret_id: str, body: UpdateSecretValueBody, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
|
|
154
|
+
"""Update a secret's value."""
|
|
155
|
+
if not body.secret_string and not body.secret_binary:
|
|
156
|
+
raise HTTPException(status_code=400, detail="Must provide either secret_string or secret_binary")
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
client = get_client("secretsmanager", **ep.client_kwargs())
|
|
160
|
+
|
|
161
|
+
kwargs: dict[str, Any] = {"SecretId": secret_id}
|
|
162
|
+
if body.secret_string:
|
|
163
|
+
kwargs["SecretString"] = body.secret_string
|
|
164
|
+
if body.secret_binary:
|
|
165
|
+
kwargs["SecretBinary"] = base64.b64decode(body.secret_binary)
|
|
166
|
+
|
|
167
|
+
resp = client.put_secret_value(**kwargs)
|
|
168
|
+
return {
|
|
169
|
+
"arn": resp["ARN"],
|
|
170
|
+
"name": resp["Name"],
|
|
171
|
+
"versionId": resp["VersionId"],
|
|
172
|
+
"versionStages": resp.get("VersionStages"),
|
|
173
|
+
}
|
|
174
|
+
except client.exceptions.ResourceNotFoundException:
|
|
175
|
+
raise HTTPException(status_code=404, detail=f"Secret '{secret_id}' not found")
|
|
176
|
+
except Exception as e:
|
|
177
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@router.put("/secrets/{secret_id:path}/metadata")
|
|
181
|
+
def update_secret_metadata(secret_id: str, body: UpdateSecretMetadataBody, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
|
|
182
|
+
"""Update a secret's description and/or tags."""
|
|
183
|
+
try:
|
|
184
|
+
client = get_client("secretsmanager", **ep.client_kwargs())
|
|
185
|
+
|
|
186
|
+
# Update description
|
|
187
|
+
if body.description is not None:
|
|
188
|
+
client.update_secret(SecretId=secret_id, Description=body.description)
|
|
189
|
+
|
|
190
|
+
# Update tags (replace all)
|
|
191
|
+
if body.tags is not None:
|
|
192
|
+
arn = client.describe_secret(SecretId=secret_id)["ARN"]
|
|
193
|
+
# Remove all existing tags first
|
|
194
|
+
existing_tags_resp = client.describe_secret(SecretId=secret_id)
|
|
195
|
+
existing_tag_keys = [tag["Key"] for tag in existing_tags_resp.get("Tags", [])]
|
|
196
|
+
if existing_tag_keys:
|
|
197
|
+
client.untag_resource(SecretId=arn, TagKeys=existing_tag_keys)
|
|
198
|
+
# Add new tags
|
|
199
|
+
if body.tags:
|
|
200
|
+
client.tag_resource(
|
|
201
|
+
SecretId=arn,
|
|
202
|
+
Tags=[{"Key": k, "Value": v} for k, v in body.tags.items()],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return {"success": True, "message": "Metadata updated"}
|
|
206
|
+
except client.exceptions.ResourceNotFoundException:
|
|
207
|
+
raise HTTPException(status_code=404, detail=f"Secret '{secret_id}' not found")
|
|
208
|
+
except Exception as e:
|
|
209
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@router.delete("/secrets/{secret_id:path}")
|
|
213
|
+
def delete_secret(
|
|
214
|
+
secret_id: str,
|
|
215
|
+
force: bool = Query(False, description="Skip recovery window (immediate deletion)"),
|
|
216
|
+
ep: EndpointInfo = Depends(get_endpoint_info),
|
|
217
|
+
) -> dict[str, Any]:
|
|
218
|
+
"""Delete a secret."""
|
|
219
|
+
try:
|
|
220
|
+
client = get_client("secretsmanager", **ep.client_kwargs())
|
|
221
|
+
|
|
222
|
+
kwargs: dict[str, Any] = {"SecretId": secret_id}
|
|
223
|
+
if force:
|
|
224
|
+
kwargs["ForceDeleteWithoutRecovery"] = True
|
|
225
|
+
else:
|
|
226
|
+
kwargs["RecoveryWindowInDays"] = 7
|
|
227
|
+
|
|
228
|
+
resp = client.delete_secret(**kwargs)
|
|
229
|
+
return {
|
|
230
|
+
"arn": resp["ARN"],
|
|
231
|
+
"name": resp["Name"],
|
|
232
|
+
"deletionDate": _format_date(resp.get("DeletionDate")),
|
|
233
|
+
}
|
|
234
|
+
except client.exceptions.ResourceNotFoundException:
|
|
235
|
+
raise HTTPException(status_code=404, detail=f"Secret '{secret_id}' not found")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@router.post("/secrets/{secret_id:path}/restore")
|
|
241
|
+
def restore_secret(secret_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
|
|
242
|
+
"""Restore a deleted secret."""
|
|
243
|
+
try:
|
|
244
|
+
client = get_client("secretsmanager", **ep.client_kwargs())
|
|
245
|
+
resp = client.restore_secret(SecretId=secret_id)
|
|
246
|
+
return {
|
|
247
|
+
"arn": resp["ARN"],
|
|
248
|
+
"name": resp["Name"],
|
|
249
|
+
}
|
|
250
|
+
except Exception as e:
|
|
251
|
+
error_code = getattr(e, "response", {}).get("Error", {}).get("Code", "")
|
|
252
|
+
if error_code == "ResourceNotFoundException":
|
|
253
|
+
raise HTTPException(status_code=404, detail=f"Secret '{secret_id}' not found")
|
|
254
|
+
elif error_code == "InvalidRequestException":
|
|
255
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
256
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Pydantic schemas for Lambda API requests."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UpdateFunctionConfigRequest(BaseModel):
|
|
7
|
+
"""Request body for updating Lambda function configuration.
|
|
8
|
+
|
|
9
|
+
All fields are optional — only specified fields will be updated.
|
|
10
|
+
Validation follows AWS Lambda limits:
|
|
11
|
+
- Memory: 128-10240 MB
|
|
12
|
+
- Timeout: 1-900 seconds
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
16
|
+
|
|
17
|
+
description: str | None = Field(None, description="Function description")
|
|
18
|
+
handler: str | None = Field(None, description="Handler path (e.g., index.handler)")
|
|
19
|
+
runtime: str | None = Field(None, description="Runtime identifier (e.g., python3.12, nodejs20.x)")
|
|
20
|
+
memory_size: int | None = Field(None, alias="memorySize", ge=128, le=10240, description="Memory in MB")
|
|
21
|
+
timeout: int | None = Field(None, ge=1, le=900, description="Timeout in seconds")
|
|
22
|
+
environment: dict[str, str] | None = Field(None, description="Environment variables as key-value dict")
|
|
23
|
+
layers: list[str] | None = Field(None, description="Layer ARNs")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Pydantic schemas for Secrets Manager API requests."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CreateSecretBody(BaseModel):
|
|
7
|
+
"""Request body for creating a new secret."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
10
|
+
|
|
11
|
+
name: str = Field(..., description="Secret name")
|
|
12
|
+
description: str | None = Field(None, description="Secret description")
|
|
13
|
+
secret_string: str | None = Field(None, alias="secretString", description="Secret value as string")
|
|
14
|
+
secret_binary: str | None = Field(None, alias="secretBinary", description="Secret value as base64-encoded binary")
|
|
15
|
+
tags: dict[str, str] = Field(default_factory=dict, description="Resource tags")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UpdateSecretValueBody(BaseModel):
|
|
19
|
+
"""Request body for updating a secret's value."""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
22
|
+
|
|
23
|
+
secret_string: str | None = Field(None, alias="secretString", description="New secret value as string")
|
|
24
|
+
secret_binary: str | None = Field(None, alias="secretBinary", description="New secret value as base64-encoded binary")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UpdateSecretMetadataBody(BaseModel):
|
|
28
|
+
"""Request body for updating a secret's metadata."""
|
|
29
|
+
|
|
30
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
31
|
+
|
|
32
|
+
description: str | None = Field(None, description="New description")
|
|
33
|
+
tags: dict[str, str] | None = Field(None, description="New tags (replaces existing)")
|
|
@@ -13,8 +13,8 @@ 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/index-
|
|
17
|
-
backend/../ui/dist/assets/index-
|
|
16
|
+
backend/../ui/dist/assets/index-BJ2zUhqY.css
|
|
17
|
+
backend/../ui/dist/assets/index-D8i-Md_b.js
|
|
18
18
|
backend/../ui/dist/aws-icons/acm.svg
|
|
19
19
|
backend/../ui/dist/aws-icons/apigateway.svg
|
|
20
20
|
backend/../ui/dist/aws-icons/appsync.svg
|
|
@@ -66,7 +66,9 @@ backend/routes/tags.py
|
|
|
66
66
|
backend/schemas/__init__.py
|
|
67
67
|
backend/schemas/dynamodb.py
|
|
68
68
|
backend/schemas/endpoints.py
|
|
69
|
+
backend/schemas/lambda_svc.py
|
|
69
70
|
backend/schemas/s3.py
|
|
71
|
+
backend/schemas/secretsmanager.py
|
|
70
72
|
backend/schemas/sqs.py
|
|
71
73
|
backend/schemas/tags.py
|
|
72
74
|
stackport.egg-info/PKG-INFO
|
|
@@ -99,8 +101,8 @@ tests/test_websocket.py
|
|
|
99
101
|
ui/dist/favicon.png
|
|
100
102
|
ui/dist/favicon.svg
|
|
101
103
|
ui/dist/index.html
|
|
102
|
-
ui/dist/assets/index-
|
|
103
|
-
ui/dist/assets/index-
|
|
104
|
+
ui/dist/assets/index-BJ2zUhqY.css
|
|
105
|
+
ui/dist/assets/index-D8i-Md_b.js
|
|
104
106
|
ui/dist/aws-icons/acm.svg
|
|
105
107
|
ui/dist/aws-icons/apigateway.svg
|
|
106
108
|
ui/dist/aws-icons/appsync.svg
|
|
@@ -269,3 +269,184 @@ class TestListVersions:
|
|
|
269
269
|
data = resp.json()
|
|
270
270
|
assert len(data["versions"]) == 2
|
|
271
271
|
assert data["versions"][0]["Version"] == "$LATEST"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestUpdateFunctionConfiguration:
|
|
275
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
276
|
+
def test_update_environment_variables(self, mock_get_client):
|
|
277
|
+
mock_lambda = MagicMock()
|
|
278
|
+
mock_get_client.return_value = mock_lambda
|
|
279
|
+
mock_lambda.update_function_configuration.return_value = {
|
|
280
|
+
"FunctionName": "my-func",
|
|
281
|
+
"Runtime": "python3.12",
|
|
282
|
+
"Handler": "handler.main",
|
|
283
|
+
"MemorySize": 256,
|
|
284
|
+
"Timeout": 30,
|
|
285
|
+
"Environment": {
|
|
286
|
+
"Variables": {
|
|
287
|
+
"KEY1": "value1",
|
|
288
|
+
"KEY2": "value2",
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
resp = client.patch(
|
|
294
|
+
"/api/lambda/functions/my-func/configuration",
|
|
295
|
+
json={
|
|
296
|
+
"environment": {
|
|
297
|
+
"KEY1": "value1",
|
|
298
|
+
"KEY2": "value2",
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
assert resp.status_code == 200
|
|
303
|
+
data = resp.json()
|
|
304
|
+
assert data["configuration"]["Environment"]["Variables"]["KEY1"] == "value1"
|
|
305
|
+
mock_lambda.update_function_configuration.assert_called_once()
|
|
306
|
+
call_args = mock_lambda.update_function_configuration.call_args[1]
|
|
307
|
+
assert call_args["FunctionName"] == "my-func"
|
|
308
|
+
assert call_args["Environment"]["Variables"] == {"KEY1": "value1", "KEY2": "value2"}
|
|
309
|
+
|
|
310
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
311
|
+
def test_update_memory_and_timeout(self, mock_get_client):
|
|
312
|
+
mock_lambda = MagicMock()
|
|
313
|
+
mock_get_client.return_value = mock_lambda
|
|
314
|
+
mock_lambda.update_function_configuration.return_value = {
|
|
315
|
+
"FunctionName": "my-func",
|
|
316
|
+
"MemorySize": 512,
|
|
317
|
+
"Timeout": 60,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
resp = client.patch(
|
|
321
|
+
"/api/lambda/functions/my-func/configuration",
|
|
322
|
+
json={"memorySize": 512, "timeout": 60},
|
|
323
|
+
)
|
|
324
|
+
assert resp.status_code == 200
|
|
325
|
+
data = resp.json()
|
|
326
|
+
assert data["configuration"]["MemorySize"] == 512
|
|
327
|
+
assert data["configuration"]["Timeout"] == 60
|
|
328
|
+
mock_lambda.update_function_configuration.assert_called_once_with(
|
|
329
|
+
FunctionName="my-func",
|
|
330
|
+
MemorySize=512,
|
|
331
|
+
Timeout=60,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
335
|
+
def test_update_handler_and_runtime(self, mock_get_client):
|
|
336
|
+
mock_lambda = MagicMock()
|
|
337
|
+
mock_get_client.return_value = mock_lambda
|
|
338
|
+
mock_lambda.update_function_configuration.return_value = {
|
|
339
|
+
"FunctionName": "my-func",
|
|
340
|
+
"Handler": "new_handler.handler",
|
|
341
|
+
"Runtime": "python3.13",
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
resp = client.patch(
|
|
345
|
+
"/api/lambda/functions/my-func/configuration",
|
|
346
|
+
json={"handler": "new_handler.handler", "runtime": "python3.13"},
|
|
347
|
+
)
|
|
348
|
+
assert resp.status_code == 200
|
|
349
|
+
data = resp.json()
|
|
350
|
+
assert data["configuration"]["Handler"] == "new_handler.handler"
|
|
351
|
+
assert data["configuration"]["Runtime"] == "python3.13"
|
|
352
|
+
|
|
353
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
354
|
+
def test_update_description(self, mock_get_client):
|
|
355
|
+
mock_lambda = MagicMock()
|
|
356
|
+
mock_get_client.return_value = mock_lambda
|
|
357
|
+
mock_lambda.update_function_configuration.return_value = {
|
|
358
|
+
"FunctionName": "my-func",
|
|
359
|
+
"Description": "Updated description",
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
resp = client.patch(
|
|
363
|
+
"/api/lambda/functions/my-func/configuration",
|
|
364
|
+
json={"description": "Updated description"},
|
|
365
|
+
)
|
|
366
|
+
assert resp.status_code == 200
|
|
367
|
+
data = resp.json()
|
|
368
|
+
assert data["configuration"]["Description"] == "Updated description"
|
|
369
|
+
|
|
370
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
371
|
+
def test_update_partial(self, mock_get_client):
|
|
372
|
+
"""Test that only specified fields are included in the update call."""
|
|
373
|
+
mock_lambda = MagicMock()
|
|
374
|
+
mock_get_client.return_value = mock_lambda
|
|
375
|
+
mock_lambda.update_function_configuration.return_value = {
|
|
376
|
+
"FunctionName": "my-func",
|
|
377
|
+
"Timeout": 120,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
resp = client.patch(
|
|
381
|
+
"/api/lambda/functions/my-func/configuration",
|
|
382
|
+
json={"timeout": 120},
|
|
383
|
+
)
|
|
384
|
+
assert resp.status_code == 200
|
|
385
|
+
# Verify only Timeout and FunctionName were passed
|
|
386
|
+
mock_lambda.update_function_configuration.assert_called_once()
|
|
387
|
+
call_args = mock_lambda.update_function_configuration.call_args[1]
|
|
388
|
+
assert set(call_args.keys()) == {"FunctionName", "Timeout"}
|
|
389
|
+
assert call_args["Timeout"] == 120
|
|
390
|
+
|
|
391
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
392
|
+
def test_update_function_not_found(self, mock_get_client):
|
|
393
|
+
mock_lambda = MagicMock()
|
|
394
|
+
mock_get_client.return_value = mock_lambda
|
|
395
|
+
# Set up both exception types
|
|
396
|
+
mock_lambda.exceptions.ResourceNotFoundException = type("ResourceNotFoundException", (Exception,), {})
|
|
397
|
+
mock_lambda.exceptions.InvalidParameterValueException = type("InvalidParameterValueException", (Exception,), {})
|
|
398
|
+
mock_lambda.update_function_configuration.side_effect = mock_lambda.exceptions.ResourceNotFoundException()
|
|
399
|
+
|
|
400
|
+
resp = client.patch(
|
|
401
|
+
"/api/lambda/functions/nonexistent/configuration",
|
|
402
|
+
json={"timeout": 60},
|
|
403
|
+
)
|
|
404
|
+
assert resp.status_code == 404
|
|
405
|
+
|
|
406
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
407
|
+
def test_update_invalid_parameter_boto3(self, mock_get_client):
|
|
408
|
+
"""Test AWS InvalidParameterValueException returns 400."""
|
|
409
|
+
mock_lambda = MagicMock()
|
|
410
|
+
mock_get_client.return_value = mock_lambda
|
|
411
|
+
# Set up both exception types
|
|
412
|
+
mock_lambda.exceptions.ResourceNotFoundException = type("ResourceNotFoundException", (Exception,), {})
|
|
413
|
+
mock_lambda.exceptions.InvalidParameterValueException = type("InvalidParameterValueException", (Exception,), {})
|
|
414
|
+
mock_lambda.update_function_configuration.side_effect = mock_lambda.exceptions.InvalidParameterValueException("Invalid runtime")
|
|
415
|
+
|
|
416
|
+
resp = client.patch(
|
|
417
|
+
"/api/lambda/functions/my-func/configuration",
|
|
418
|
+
json={"runtime": "invalid_runtime"},
|
|
419
|
+
)
|
|
420
|
+
assert resp.status_code == 400
|
|
421
|
+
|
|
422
|
+
def test_update_validation_memory_too_low(self):
|
|
423
|
+
"""Test Pydantic validation rejects memory < 128."""
|
|
424
|
+
resp = client.patch(
|
|
425
|
+
"/api/lambda/functions/my-func/configuration",
|
|
426
|
+
json={"memorySize": 64},
|
|
427
|
+
)
|
|
428
|
+
assert resp.status_code == 422
|
|
429
|
+
|
|
430
|
+
def test_update_validation_memory_too_high(self):
|
|
431
|
+
"""Test Pydantic validation rejects memory > 10240."""
|
|
432
|
+
resp = client.patch(
|
|
433
|
+
"/api/lambda/functions/my-func/configuration",
|
|
434
|
+
json={"memorySize": 20480},
|
|
435
|
+
)
|
|
436
|
+
assert resp.status_code == 422
|
|
437
|
+
|
|
438
|
+
def test_update_validation_timeout_too_low(self):
|
|
439
|
+
"""Test Pydantic validation rejects timeout < 1."""
|
|
440
|
+
resp = client.patch(
|
|
441
|
+
"/api/lambda/functions/my-func/configuration",
|
|
442
|
+
json={"timeout": 0},
|
|
443
|
+
)
|
|
444
|
+
assert resp.status_code == 422
|
|
445
|
+
|
|
446
|
+
def test_update_validation_timeout_too_high(self):
|
|
447
|
+
"""Test Pydantic validation rejects timeout > 900."""
|
|
448
|
+
resp = client.patch(
|
|
449
|
+
"/api/lambda/functions/my-func/configuration",
|
|
450
|
+
json={"timeout": 1000},
|
|
451
|
+
)
|
|
452
|
+
assert resp.status_code == 422
|