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.
Files changed (105) hide show
  1. {stackport-0.3.0/stackport.egg-info → stackport-0.3.2}/PKG-INFO +1 -1
  2. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/lambda_svc.py +52 -0
  3. stackport-0.3.2/backend/routes/secretsmanager.py +256 -0
  4. stackport-0.3.2/backend/schemas/lambda_svc.py +23 -0
  5. stackport-0.3.2/backend/schemas/secretsmanager.py +33 -0
  6. {stackport-0.3.0 → stackport-0.3.2}/pyproject.toml +1 -1
  7. {stackport-0.3.0 → stackport-0.3.2/stackport.egg-info}/PKG-INFO +1 -1
  8. {stackport-0.3.0 → stackport-0.3.2}/stackport.egg-info/SOURCES.txt +6 -4
  9. {stackport-0.3.0 → stackport-0.3.2}/tests/test_lambda_routes.py +181 -0
  10. {stackport-0.3.0 → stackport-0.3.2}/tests/test_secretsmanager_routes.py +227 -0
  11. stackport-0.3.2/ui/dist/assets/index-BJ2zUhqY.css +1 -0
  12. stackport-0.3.2/ui/dist/assets/index-D8i-Md_b.js +639 -0
  13. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/index.html +2 -2
  14. stackport-0.3.0/backend/routes/secretsmanager.py +0 -113
  15. stackport-0.3.0/ui/dist/assets/index-Cda_0qu5.css +0 -1
  16. stackport-0.3.0/ui/dist/assets/index-tW-8e4eS.js +0 -629
  17. {stackport-0.3.0 → stackport-0.3.2}/LICENSE +0 -0
  18. {stackport-0.3.0 → stackport-0.3.2}/MANIFEST.in +0 -0
  19. {stackport-0.3.0 → stackport-0.3.2}/README.md +0 -0
  20. {stackport-0.3.0 → stackport-0.3.2}/backend/__init__.py +0 -0
  21. {stackport-0.3.0 → stackport-0.3.2}/backend/aws_client.py +0 -0
  22. {stackport-0.3.0 → stackport-0.3.2}/backend/cache.py +0 -0
  23. {stackport-0.3.0 → stackport-0.3.2}/backend/cli.py +0 -0
  24. {stackport-0.3.0 → stackport-0.3.2}/backend/config.py +0 -0
  25. {stackport-0.3.0 → stackport-0.3.2}/backend/endpoint_store.py +0 -0
  26. {stackport-0.3.0 → stackport-0.3.2}/backend/main.py +0 -0
  27. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/__init__.py +0 -0
  28. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/common.py +0 -0
  29. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/dynamodb.py +0 -0
  30. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/ec2.py +0 -0
  31. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/endpoints.py +0 -0
  32. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/iam.py +0 -0
  33. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/logs.py +0 -0
  34. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/resources.py +0 -0
  35. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/s3.py +0 -0
  36. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/sqs.py +0 -0
  37. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/stats.py +0 -0
  38. {stackport-0.3.0 → stackport-0.3.2}/backend/routes/tags.py +0 -0
  39. {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/__init__.py +0 -0
  40. {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/dynamodb.py +0 -0
  41. {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/endpoints.py +0 -0
  42. {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/s3.py +0 -0
  43. {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/sqs.py +0 -0
  44. {stackport-0.3.0 → stackport-0.3.2}/backend/schemas/tags.py +0 -0
  45. {stackport-0.3.0 → stackport-0.3.2}/backend/websocket.py +0 -0
  46. {stackport-0.3.0 → stackport-0.3.2}/setup.cfg +0 -0
  47. {stackport-0.3.0 → stackport-0.3.2}/stackport.egg-info/dependency_links.txt +0 -0
  48. {stackport-0.3.0 → stackport-0.3.2}/stackport.egg-info/entry_points.txt +0 -0
  49. {stackport-0.3.0 → stackport-0.3.2}/stackport.egg-info/requires.txt +0 -0
  50. {stackport-0.3.0 → stackport-0.3.2}/stackport.egg-info/top_level.txt +0 -0
  51. {stackport-0.3.0 → stackport-0.3.2}/tests/test_cache.py +0 -0
  52. {stackport-0.3.0 → stackport-0.3.2}/tests/test_cli.py +0 -0
  53. {stackport-0.3.0 → stackport-0.3.2}/tests/test_client.py +0 -0
  54. {stackport-0.3.0 → stackport-0.3.2}/tests/test_config.py +0 -0
  55. {stackport-0.3.0 → stackport-0.3.2}/tests/test_dynamodb_routes.py +0 -0
  56. {stackport-0.3.0 → stackport-0.3.2}/tests/test_ec2_routes.py +0 -0
  57. {stackport-0.3.0 → stackport-0.3.2}/tests/test_endpoint_store.py +0 -0
  58. {stackport-0.3.0 → stackport-0.3.2}/tests/test_endpoints.py +0 -0
  59. {stackport-0.3.0 → stackport-0.3.2}/tests/test_iam_routes.py +0 -0
  60. {stackport-0.3.0 → stackport-0.3.2}/tests/test_logs_routes.py +0 -0
  61. {stackport-0.3.0 → stackport-0.3.2}/tests/test_multi_endpoint.py +0 -0
  62. {stackport-0.3.0 → stackport-0.3.2}/tests/test_readonly_middleware.py +0 -0
  63. {stackport-0.3.0 → stackport-0.3.2}/tests/test_registries.py +0 -0
  64. {stackport-0.3.0 → stackport-0.3.2}/tests/test_routes.py +0 -0
  65. {stackport-0.3.0 → stackport-0.3.2}/tests/test_s3_routes.py +0 -0
  66. {stackport-0.3.0 → stackport-0.3.2}/tests/test_s3_upload_limit_env.py +0 -0
  67. {stackport-0.3.0 → stackport-0.3.2}/tests/test_sqs_routes.py +0 -0
  68. {stackport-0.3.0 → stackport-0.3.2}/tests/test_tags_routes.py +0 -0
  69. {stackport-0.3.0 → stackport-0.3.2}/tests/test_websocket.py +0 -0
  70. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/acm.svg +0 -0
  71. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/apigateway.svg +0 -0
  72. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/appsync.svg +0 -0
  73. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/athena.svg +0 -0
  74. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/cloudformation.svg +0 -0
  75. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/cloudfront.svg +0 -0
  76. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  77. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/dynamodb.svg +0 -0
  78. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/ec2.svg +0 -0
  79. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/ecr.svg +0 -0
  80. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/ecs.svg +0 -0
  81. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/elasticache.svg +0 -0
  82. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  83. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  84. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  85. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/events.svg +0 -0
  86. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/firehose.svg +0 -0
  87. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/glue.svg +0 -0
  88. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/iam.svg +0 -0
  89. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/kinesis.svg +0 -0
  90. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/kms.svg +0 -0
  91. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/lambda.svg +0 -0
  92. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/logs.svg +0 -0
  93. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/monitoring.svg +0 -0
  94. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/rds.svg +0 -0
  95. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/route53.svg +0 -0
  96. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/s3.svg +0 -0
  97. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  98. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/ses.svg +0 -0
  99. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/sns.svg +0 -0
  100. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/sqs.svg +0 -0
  101. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/ssm.svg +0 -0
  102. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  103. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/aws-icons/wafv2.svg +0 -0
  104. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/favicon.png +0 -0
  105. {stackport-0.3.0 → stackport-0.3.2}/ui/dist/favicon.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -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)")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackport"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Universal AWS resource browser for local emulators"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -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-Cda_0qu5.css
17
- backend/../ui/dist/assets/index-tW-8e4eS.js
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-Cda_0qu5.css
103
- ui/dist/assets/index-tW-8e4eS.js
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