stackport 0.4.7__tar.gz → 0.4.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.4.7/stackport.egg-info → stackport-0.4.9}/PKG-INFO +6 -5
- {stackport-0.4.7 → stackport-0.4.9}/README.md +3 -2
- {stackport-0.4.7 → stackport-0.4.9}/backend/aws_client.py +10 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/cli.py +59 -81
- {stackport-0.4.7 → stackport-0.4.9}/backend/config.py +9 -2
- {stackport-0.4.7 → stackport-0.4.9}/backend/connection.py +28 -7
- stackport-0.4.9/backend/downloads.py +64 -0
- stackport-0.4.9/backend/learn/cli_setup.py +114 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/learn/verify.py +2 -7
- {stackport-0.4.7 → stackport-0.4.9}/backend/main.py +16 -2
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/common.py +22 -20
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/dynamodb.py +184 -171
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/endpoints.py +3 -2
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/iam.py +17 -15
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/lambda_svc.py +8 -35
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/learn.py +12 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/logs.py +27 -30
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/resources.py +123 -96
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/s3.py +49 -14
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/sqs.py +48 -31
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/stats.py +51 -11
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/tags.py +51 -20
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/sqs.py +35 -23
- {stackport-0.4.7 → stackport-0.4.9}/backend/websocket.py +127 -61
- {stackport-0.4.7 → stackport-0.4.9}/pyproject.toml +3 -3
- {stackport-0.4.7 → stackport-0.4.9/stackport.egg-info}/PKG-INFO +6 -5
- {stackport-0.4.7 → stackport-0.4.9}/stackport.egg-info/SOURCES.txt +34 -26
- {stackport-0.4.7 → stackport-0.4.9}/stackport.egg-info/requires.txt +2 -2
- stackport-0.4.9/tests/test_aws_client_checksums.py +77 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_aws_client_tls.py +43 -0
- stackport-0.4.9/tests/test_cli.py +370 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_connection.py +95 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_dynamodb_routes.py +281 -9
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_endpoint_store_env.py +10 -0
- stackport-0.4.9/tests/test_generic_resources.py +514 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_iam_routes.py +100 -181
- stackport-0.4.9/tests/test_learn_cli_setup.py +153 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_logs_routes.py +87 -2
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_logs_tail.py +2 -1
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_multi_endpoint.py +12 -8
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_registries.py +32 -1
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_request_safety.py +32 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_s3_routes.py +137 -152
- stackport-0.4.9/tests/test_s3_transfers.py +285 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_sqs_routes.py +162 -0
- stackport-0.4.9/tests/test_suite_isolation.py +41 -0
- stackport-0.4.9/tests/test_tags_roundtrip.py +409 -0
- stackport-0.4.9/tests/test_websocket.py +409 -0
- stackport-0.4.7/ui/dist/assets/CloudscapeAbout-C464xiLP.js → stackport-0.4.9/ui/dist/assets/CloudscapeAbout-C4S2ahmA.js +1 -1
- stackport-0.4.7/ui/dist/assets/CloudscapeDashboard--Puj8I6B.js → stackport-0.4.9/ui/dist/assets/CloudscapeDashboard-CgPMF-G_.js +1 -1
- stackport-0.4.7/ui/dist/assets/CloudscapeLearn-CT7ucW_g.js → stackport-0.4.9/ui/dist/assets/CloudscapeLearn-B8fMSiDF.js +1 -1
- stackport-0.4.9/ui/dist/assets/CloudscapeResourceBrowser-IhkIJ1Hg.js +19 -0
- stackport-0.4.7/ui/dist/assets/CloudscapeSettings-D_9KUPvP.js → stackport-0.4.9/ui/dist/assets/CloudscapeSettings-D4ZhF-Od.js +1 -1
- stackport-0.4.7/ui/dist/assets/CloudscapeShell-BCyqqtsn.js → stackport-0.4.9/ui/dist/assets/CloudscapeShell-C7gsElS6.js +1 -1
- stackport-0.4.7/ui/dist/assets/ServiceStatus-KR6X2qeu.js → stackport-0.4.9/ui/dist/assets/ServiceStatus-Bn8D3Whz.js +1 -1
- stackport-0.4.7/ui/dist/assets/StateMachineGraph-D9IP3035.js → stackport-0.4.9/ui/dist/assets/StateMachineGraph-DBZaV-ye.js +1 -1
- stackport-0.4.7/ui/dist/assets/index-CHdSsqYw.js → stackport-0.4.9/ui/dist/assets/index-BIP6RNbD.js +1 -1
- stackport-0.4.7/ui/dist/assets/index-WR8pyfQO.js → stackport-0.4.9/ui/dist/assets/index-BOjNdz1a.js +1 -1
- stackport-0.4.7/ui/dist/assets/index-WNsJzp13.js → stackport-0.4.9/ui/dist/assets/index-C56KgCLT.js +1 -1
- stackport-0.4.7/ui/dist/assets/index-DKnYLurL.js → stackport-0.4.9/ui/dist/assets/index-R-ZHGMoF.js +1 -1
- stackport-0.4.9/ui/dist/assets/index-bfBTZg8t.js +67 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/index.html +1 -1
- stackport-0.4.7/tests/test_cli.py +0 -211
- stackport-0.4.7/tests/test_websocket.py +0 -58
- stackport-0.4.7/ui/dist/assets/CloudscapeResourceBrowser-CprDqhcQ.js +0 -19
- stackport-0.4.7/ui/dist/assets/index-CvtAnA9F.js +0 -67
- {stackport-0.4.7 → stackport-0.4.9}/LICENSE +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/MANIFEST.in +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/__init__.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/aws_emulator.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/aws_errors.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/cache.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/emulators.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/endpoint_store.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/error_handlers.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/learn/__init__.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/learn/content.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/learn/hotspots.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/learn/trails/aws-first-steps.json +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/learn_store.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/origins.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/__init__.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/ec2.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/kms.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/monitoring.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/rds.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/secretsmanager.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/sns.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/routes/stepfunctions.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/__init__.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/dynamodb.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/endpoints.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/lambda_svc.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/learn.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/logs.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/monitoring.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/s3.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/secretsmanager.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/sns.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/stepfunctions.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/backend/schemas/tags.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/setup.cfg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/stackport.egg-info/top_level.txt +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_aws_emulator.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_aws_errors.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_cache.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_client.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_config.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_create_resources.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_ec2_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_emulators.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_endpoint_store.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_endpoints.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_endpoints_tls.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_error_handlers.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_kms_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_lambda_code_download.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_lambda_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_learn_content.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_learn_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_learn_store.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_monitoring_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_rds_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_readonly_middleware.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_s3_upload_limit_env.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_secretsmanager_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_sns_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_stats_probe.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/tests/test_tags_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/assets/CloudscapeResourceBrowser-BR6m7Ajg.css +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/assets/CloudscapeSettings-DI0pOcAA.css +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/assets/CloudscapeShell-oIMuVycJ.css +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/assets/ServiceStatus-DNBdeKId.css +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/assets/index-DKIdXFX0.css +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/assets/index-D__irddE.css +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/assets/index-DoY0HscW.css +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/assets/index-RwoKZF4y.css +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/assets/utils-Dm4tJbJR.js +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/acm.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/apigateway.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/appsync.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/athena.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/cloudformation.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/cloudfront.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/cognito-idp.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/dynamodb.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/ec2.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/ecr.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/ecs.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/elasticache.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/events.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/firehose.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/glue.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/iam.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/kinesis.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/kms.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/lambda.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/logs.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/monitoring.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/rds.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/route53.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/s3.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/secretsmanager.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/ses.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/sns.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/sqs.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/ssm.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/stepfunctions.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/aws-icons/wafv2.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/favicon.png +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/favicon.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.9}/ui/dist/stackport-logo.svg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackport
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.9
|
|
4
4
|
Summary: Universal AWS resource browser for local emulators
|
|
5
5
|
Author: Davi Reis Vieira
|
|
6
6
|
License: MIT
|
|
@@ -23,7 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: fastapi>=0.115.0
|
|
25
25
|
Requires-Dist: uvicorn>=0.30.0
|
|
26
|
-
Requires-Dist: boto3>=1.
|
|
26
|
+
Requires-Dist: boto3>=1.36.0
|
|
27
27
|
Requires-Dist: click>=8.0
|
|
28
28
|
Requires-Dist: websockets>=12.0
|
|
29
29
|
Requires-Dist: python-multipart>=0.0.26
|
|
@@ -31,7 +31,7 @@ Provides-Extra: dev
|
|
|
31
31
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
32
32
|
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
|
33
33
|
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
34
|
-
Requires-Dist: moto[dynamodb,iam,lambda,s3,sqs]>=5.0; extra == "dev"
|
|
34
|
+
Requires-Dist: moto[apigateway,apigatewayv2,cognitoidp,dynamodb,glue,iam,lambda,s3,sqs]>=5.0; extra == "dev"
|
|
35
35
|
Dynamic: license-file
|
|
36
36
|
|
|
37
37
|
<p align="center">
|
|
@@ -249,7 +249,7 @@ You can test a connection before saving it with the "Test Connection" button in
|
|
|
249
249
|
| Service | Browse | Write operations |
|
|
250
250
|
|---------|--------|------------------|
|
|
251
251
|
| **S3** | Buckets, objects, folder navigation, search | Create buckets and folders, upload, download, delete, batch delete; edit versioning, lifecycle, notifications, CORS, tags |
|
|
252
|
-
| **DynamoDB** | Tables, schema, items, scan and query | Create tables, add, edit and delete items, batch write |
|
|
252
|
+
| **DynamoDB** | Tables, schema, items, scan and query, live item count | Create tables, add, edit and delete items, batch write |
|
|
253
253
|
| **Lambda** | Functions, config, aliases, versions, event sources | Invoke with a JSON payload, edit configuration, download code |
|
|
254
254
|
| **SQS** | Queues, messages, attributes | Create and delete queues, send (single or batch), receive, delete messages, purge, edit attributes and redrive policy |
|
|
255
255
|
| **SNS** | Topics, subscriptions | Create and delete topics, subscribe, unsubscribe, publish |
|
|
@@ -314,7 +314,7 @@ stackport export lambda --format json
|
|
|
314
314
|
stackport export ec2 --format csv
|
|
315
315
|
```
|
|
316
316
|
|
|
317
|
-
`status`, `list`, `describe`, and `export` accept `--endpoint URL
|
|
317
|
+
`status`, `list`, `describe`, and `export` accept `--endpoint` (a saved endpoint name or a URL) and `--region REGION`. Without `--endpoint` they use `AWS_ENDPOINT_URL` or the default endpoint saved in Settings. A saved endpoint brings its profile or keys, region and TLS settings (CA bundle, verification off) with it, and `--region` overrides its region. A URL passed directly uses no saved settings.
|
|
318
318
|
|
|
319
319
|
## Keyboard shortcuts
|
|
320
320
|
|
|
@@ -341,6 +341,7 @@ Press `?` anywhere to see all shortcuts.
|
|
|
341
341
|
| `AWS_REGION` | `us-east-1` | AWS region |
|
|
342
342
|
| `AWS_ACCESS_KEY_ID` | *(unset)* | AWS access key. Unset = use credential chain |
|
|
343
343
|
| `AWS_SECRET_ACCESS_KEY` | *(unset)* | AWS secret key. Unset = use credential chain |
|
|
344
|
+
| `AWS_SESSION_TOKEN` | *(unset)* | Session token for temporary credentials (SSO exports, assumed roles). Sent only with the two keys above. Read at start-up, so a new token needs a restart |
|
|
344
345
|
| `AWS_PROFILE` | *(unset)* | AWS named profile from `~/.aws/config` |
|
|
345
346
|
| `STACKPORT_PORT` | `8080` | StackPort server port |
|
|
346
347
|
| `STACKPORT_HOST` | `127.0.0.1` | Interface StackPort listens on (`0.0.0.0` in the Docker image). StackPort has no login, so open it to the network only where that's fine |
|
|
@@ -213,7 +213,7 @@ You can test a connection before saving it with the "Test Connection" button in
|
|
|
213
213
|
| Service | Browse | Write operations |
|
|
214
214
|
|---------|--------|------------------|
|
|
215
215
|
| **S3** | Buckets, objects, folder navigation, search | Create buckets and folders, upload, download, delete, batch delete; edit versioning, lifecycle, notifications, CORS, tags |
|
|
216
|
-
| **DynamoDB** | Tables, schema, items, scan and query | Create tables, add, edit and delete items, batch write |
|
|
216
|
+
| **DynamoDB** | Tables, schema, items, scan and query, live item count | Create tables, add, edit and delete items, batch write |
|
|
217
217
|
| **Lambda** | Functions, config, aliases, versions, event sources | Invoke with a JSON payload, edit configuration, download code |
|
|
218
218
|
| **SQS** | Queues, messages, attributes | Create and delete queues, send (single or batch), receive, delete messages, purge, edit attributes and redrive policy |
|
|
219
219
|
| **SNS** | Topics, subscriptions | Create and delete topics, subscribe, unsubscribe, publish |
|
|
@@ -278,7 +278,7 @@ stackport export lambda --format json
|
|
|
278
278
|
stackport export ec2 --format csv
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
-
`status`, `list`, `describe`, and `export` accept `--endpoint URL
|
|
281
|
+
`status`, `list`, `describe`, and `export` accept `--endpoint` (a saved endpoint name or a URL) and `--region REGION`. Without `--endpoint` they use `AWS_ENDPOINT_URL` or the default endpoint saved in Settings. A saved endpoint brings its profile or keys, region and TLS settings (CA bundle, verification off) with it, and `--region` overrides its region. A URL passed directly uses no saved settings.
|
|
282
282
|
|
|
283
283
|
## Keyboard shortcuts
|
|
284
284
|
|
|
@@ -305,6 +305,7 @@ Press `?` anywhere to see all shortcuts.
|
|
|
305
305
|
| `AWS_REGION` | `us-east-1` | AWS region |
|
|
306
306
|
| `AWS_ACCESS_KEY_ID` | *(unset)* | AWS access key. Unset = use credential chain |
|
|
307
307
|
| `AWS_SECRET_ACCESS_KEY` | *(unset)* | AWS secret key. Unset = use credential chain |
|
|
308
|
+
| `AWS_SESSION_TOKEN` | *(unset)* | Session token for temporary credentials (SSO exports, assumed roles). Sent only with the two keys above. Read at start-up, so a new token needs a restart |
|
|
308
309
|
| `AWS_PROFILE` | *(unset)* | AWS named profile from `~/.aws/config` |
|
|
309
310
|
| `STACKPORT_PORT` | `8080` | StackPort server port |
|
|
310
311
|
| `STACKPORT_HOST` | `127.0.0.1` | Interface StackPort listens on (`0.0.0.0` in the Docker image). StackPort has no login, so open it to the network only where that's fine |
|
|
@@ -26,10 +26,18 @@ _UNSET = object()
|
|
|
26
26
|
# dozens of services. Locally nothing is throttled, a refused connection or a
|
|
27
27
|
# rejected certificate won't fix itself on retry, and the UI polls again in
|
|
28
28
|
# seconds, so fail fast instead. STACKPORT_PROBE_TIMEOUT bounds the connect.
|
|
29
|
+
# - Flexible checksums (CRC32 on uploads, checked on downloads) only where an API
|
|
30
|
+
# requires them, as before botocore 1.36. Emulators keep them unevenly: moto
|
|
31
|
+
# returns a multipart object's checksum of part checksums as if it covered the
|
|
32
|
+
# whole object, so a download of it failed its check. Without asking for them,
|
|
33
|
+
# StackPort's multipart uploads don't leave such objects behind for other
|
|
34
|
+
# clients either.
|
|
29
35
|
_LOCAL_ENDPOINT_CONFIG = Config(
|
|
30
36
|
inject_host_prefix=False,
|
|
31
37
|
connect_timeout=STACKPORT_PROBE_TIMEOUT,
|
|
32
38
|
retries={"mode": "standard", "total_max_attempts": 1},
|
|
39
|
+
request_checksum_calculation="when_required",
|
|
40
|
+
response_checksum_validation="when_required",
|
|
33
41
|
)
|
|
34
42
|
|
|
35
43
|
# Last stat stamp seen per AWS Emulator files directory, to notice restarts.
|
|
@@ -47,6 +55,8 @@ def _build_client(service_name: str, plan: ConnectionPlan):
|
|
|
47
55
|
session_kwargs["aws_access_key_id"] = plan.access_key_id
|
|
48
56
|
if plan.secret_access_key is not None:
|
|
49
57
|
session_kwargs["aws_secret_access_key"] = plan.secret_access_key
|
|
58
|
+
if plan.session_token is not None:
|
|
59
|
+
session_kwargs["aws_session_token"] = plan.session_token
|
|
50
60
|
session = boto3.Session(**session_kwargs)
|
|
51
61
|
|
|
52
62
|
client_kwargs: dict = {"service_name": service_name}
|
|
@@ -12,34 +12,64 @@ import uvicorn
|
|
|
12
12
|
from backend.aws_client import get_client
|
|
13
13
|
from backend.config import (
|
|
14
14
|
AWS_ENDPOINT_URL,
|
|
15
|
-
|
|
15
|
+
ENDPOINTS,
|
|
16
16
|
LOG_LEVEL,
|
|
17
17
|
STACKPORT_HOST,
|
|
18
18
|
STACKPORT_PORT,
|
|
19
19
|
STACKPORT_SERVICES,
|
|
20
20
|
endpoint_store,
|
|
21
21
|
)
|
|
22
|
+
from backend.connection import EndpointInfo, InvalidEndpointUrl
|
|
23
|
+
from backend.routes.common import UnknownEndpoint, endpoint_info_for
|
|
22
24
|
from backend.routes.resources import (
|
|
23
|
-
DESCRIBE_REGISTRY,
|
|
24
25
|
_PREFERRED_ID_FIELD,
|
|
26
|
+
UnknownResourceType,
|
|
25
27
|
_extract_id,
|
|
26
28
|
_serialize,
|
|
29
|
+
describe_resource,
|
|
27
30
|
)
|
|
28
|
-
from backend.routes.stats import SERVICE_REGISTRY,
|
|
31
|
+
from backend.routes.stats import SERVICE_REGISTRY, _probe_service, list_items
|
|
29
32
|
|
|
30
33
|
logger = logging.getLogger(__name__)
|
|
31
34
|
|
|
32
35
|
|
|
36
|
+
def _startup_endpoint_info() -> EndpointInfo:
|
|
37
|
+
"""Connection info for AWS_ENDPOINT_URL, with the settings of the env endpoint it defines.
|
|
38
|
+
|
|
39
|
+
The store points that env endpoint (normally "default") at the environment's URL
|
|
40
|
+
for the run. If it was deleted, the URL is used on its own.
|
|
41
|
+
"""
|
|
42
|
+
name = next((n for n, url in ENDPOINTS.items() if url == AWS_ENDPOINT_URL), None)
|
|
43
|
+
entry = endpoint_store.get(name) if name is not None else None
|
|
44
|
+
if entry is not None and entry["source"] == "env" and entry["url"] == AWS_ENDPOINT_URL:
|
|
45
|
+
return EndpointInfo.from_entry(entry)
|
|
46
|
+
return endpoint_info_for(AWS_ENDPOINT_URL)
|
|
47
|
+
|
|
48
|
+
|
|
33
49
|
def _client_kwargs(endpoint: str | None, region: str | None) -> dict:
|
|
34
|
-
"""Resolve where a CLI command talks to.
|
|
50
|
+
"""Resolve where a CLI command talks to, the way the web app does.
|
|
51
|
+
|
|
52
|
+
``endpoint`` is a saved endpoint's name or a URL. Without it, AWS_ENDPOINT_URL
|
|
53
|
+
at startup wins (with the settings of the env endpoint that defines it), then
|
|
54
|
+
the saved default endpoint. A saved endpoint brings its auth and TLS settings
|
|
55
|
+
along; an explicit ``region`` overrides its region. Passing the result to
|
|
56
|
+
get_client explicitly is what makes the flag work once
|
|
57
|
+
~/.stackport/endpoints.json exists.
|
|
35
58
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Passing the result to get_client explicitly is what makes the flag work
|
|
39
|
-
once ~/.stackport/endpoints.json exists.
|
|
59
|
+
Raises:
|
|
60
|
+
click.BadParameter: the name matches no saved endpoint, or the URL is refused.
|
|
40
61
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
try:
|
|
63
|
+
if endpoint is None and AWS_ENDPOINT_URL is not None:
|
|
64
|
+
info = _startup_endpoint_info()
|
|
65
|
+
else:
|
|
66
|
+
info = endpoint_info_for(endpoint, strict=True)
|
|
67
|
+
except (InvalidEndpointUrl, UnknownEndpoint) as exc:
|
|
68
|
+
raise click.BadParameter(str(exc), param_hint="--endpoint") from exc
|
|
69
|
+
kwargs = info.client_kwargs()
|
|
70
|
+
if region is not None:
|
|
71
|
+
kwargs["region"] = region
|
|
72
|
+
return kwargs
|
|
43
73
|
|
|
44
74
|
|
|
45
75
|
@click.group(invoke_without_command=True)
|
|
@@ -60,8 +90,8 @@ def serve(port, host):
|
|
|
60
90
|
|
|
61
91
|
|
|
62
92
|
@cli.command()
|
|
63
|
-
@click.option("--endpoint", default=
|
|
64
|
-
@click.option("--region", default=
|
|
93
|
+
@click.option("--endpoint", default=None, help="Saved endpoint name or endpoint URL (default: AWS_ENDPOINT_URL if set, else the saved default endpoint)")
|
|
94
|
+
@click.option("--region", default=None, help="AWS region (default: the endpoint's region, then AWS_REGION)")
|
|
65
95
|
@click.option("--output", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
66
96
|
def status(endpoint, region, output):
|
|
67
97
|
"""Show all services with availability and resource counts."""
|
|
@@ -102,8 +132,8 @@ def status(endpoint, region, output):
|
|
|
102
132
|
|
|
103
133
|
@cli.command()
|
|
104
134
|
@click.argument("service")
|
|
105
|
-
@click.option("--endpoint", default=
|
|
106
|
-
@click.option("--region", default=
|
|
135
|
+
@click.option("--endpoint", default=None, help="Saved endpoint name or endpoint URL (default: AWS_ENDPOINT_URL if set, else the saved default endpoint)")
|
|
136
|
+
@click.option("--region", default=None, help="AWS region (default: the endpoint's region, then AWS_REGION)")
|
|
107
137
|
@click.option("--output", type=click.Choice(["json", "table", "csv"]), default="table", help="Output format")
|
|
108
138
|
def list(service, endpoint, region, output):
|
|
109
139
|
"""List resources for a service."""
|
|
@@ -120,14 +150,7 @@ def list(service, endpoint, region, output):
|
|
|
120
150
|
for resource_type, boto3_service, method_name, response_key in registry_entries:
|
|
121
151
|
try:
|
|
122
152
|
client = get_client(boto3_service, **client_kwargs)
|
|
123
|
-
|
|
124
|
-
kwargs = _METHOD_KWARGS.get((boto3_service, method_name), {})
|
|
125
|
-
resp = method(**kwargs)
|
|
126
|
-
items = resp.get(response_key, [])
|
|
127
|
-
# Handle nested structures (e.g., cloudfront DistributionList.Items)
|
|
128
|
-
if isinstance(items, dict) and "Items" in items:
|
|
129
|
-
items = items.get("Items", []) or []
|
|
130
|
-
resources[resource_type] = items
|
|
153
|
+
resources[resource_type] = list_items(client, boto3_service, method_name, response_key)
|
|
131
154
|
except Exception as exc:
|
|
132
155
|
logger.debug("Failed to list %s/%s: %s", service, resource_type, exc, exc_info=True)
|
|
133
156
|
resources[resource_type] = []
|
|
@@ -171,68 +194,23 @@ def list(service, endpoint, region, output):
|
|
|
171
194
|
@click.argument("service")
|
|
172
195
|
@click.argument("resource_type")
|
|
173
196
|
@click.argument("resource_id")
|
|
174
|
-
@click.option("--endpoint", default=
|
|
175
|
-
@click.option("--region", default=
|
|
197
|
+
@click.option("--endpoint", default=None, help="Saved endpoint name or endpoint URL (default: AWS_ENDPOINT_URL if set, else the saved default endpoint)")
|
|
198
|
+
@click.option("--region", default=None, help="AWS region (default: the endpoint's region, then AWS_REGION)")
|
|
176
199
|
@click.option("--output", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
177
200
|
def describe(service, resource_type, resource_id, endpoint, region, output):
|
|
178
201
|
"""Describe a specific resource."""
|
|
179
202
|
client_kwargs = _client_kwargs(endpoint, region)
|
|
180
203
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
resp.pop("ResponseMetadata", None)
|
|
192
|
-
detail = _serialize(resp.get("WebACL", resp))
|
|
193
|
-
except Exception as exc:
|
|
194
|
-
click.echo(f"Error describing {service}/{resource_type}/{resource_id}: {exc}", err=True)
|
|
195
|
-
sys.exit(1)
|
|
196
|
-
else:
|
|
197
|
-
lookup = DESCRIBE_REGISTRY.get((service, resource_type))
|
|
198
|
-
if not lookup:
|
|
199
|
-
click.echo(f"Error: No detail lookup registered for {service}/{resource_type}", err=True)
|
|
200
|
-
sys.exit(1)
|
|
201
|
-
|
|
202
|
-
boto3_service, method_name, id_param, response_key = lookup
|
|
203
|
-
|
|
204
|
-
# Some APIs take list parameters
|
|
205
|
-
_LIST_PARAMS = {
|
|
206
|
-
"InstanceIds",
|
|
207
|
-
"VpcIds",
|
|
208
|
-
"SubnetIds",
|
|
209
|
-
"GroupIds",
|
|
210
|
-
"VolumeIds",
|
|
211
|
-
"repositoryNames",
|
|
212
|
-
"clusters",
|
|
213
|
-
"AlarmNames",
|
|
214
|
-
"LoadBalancerArns",
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
client = get_client(boto3_service, **client_kwargs)
|
|
219
|
-
method = getattr(client, method_name)
|
|
220
|
-
if id_param in _LIST_PARAMS:
|
|
221
|
-
resp = method(**{id_param: [resource_id]})
|
|
222
|
-
else:
|
|
223
|
-
resp = method(**{id_param: resource_id})
|
|
224
|
-
|
|
225
|
-
resp.pop("ResponseMetadata", None)
|
|
226
|
-
|
|
227
|
-
if response_key is not None:
|
|
228
|
-
detail = resp.get(response_key, resp)
|
|
229
|
-
else:
|
|
230
|
-
detail = resp
|
|
231
|
-
|
|
232
|
-
detail = _serialize(detail)
|
|
233
|
-
except Exception as exc:
|
|
234
|
-
click.echo(f"Error describing {service}/{resource_type}/{resource_id}: {exc}", err=True)
|
|
235
|
-
sys.exit(1)
|
|
204
|
+
try:
|
|
205
|
+
detail = describe_resource(
|
|
206
|
+
lambda boto3_service: get_client(boto3_service, **client_kwargs), service, resource_type, resource_id
|
|
207
|
+
)
|
|
208
|
+
except UnknownResourceType as exc:
|
|
209
|
+
click.echo(f"Error: {exc}", err=True)
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
click.echo(f"Error describing {service}/{resource_type}/{resource_id}: {exc}", err=True)
|
|
213
|
+
sys.exit(1)
|
|
236
214
|
|
|
237
215
|
if output == "json":
|
|
238
216
|
click.echo(json.dumps(detail, indent=2))
|
|
@@ -253,8 +231,8 @@ def describe(service, resource_type, resource_id, endpoint, region, output):
|
|
|
253
231
|
|
|
254
232
|
@cli.command()
|
|
255
233
|
@click.argument("service")
|
|
256
|
-
@click.option("--endpoint", default=
|
|
257
|
-
@click.option("--region", default=
|
|
234
|
+
@click.option("--endpoint", default=None, help="Saved endpoint name or endpoint URL (default: AWS_ENDPOINT_URL if set, else the saved default endpoint)")
|
|
235
|
+
@click.option("--region", default=None, help="AWS region (default: the endpoint's region, then AWS_REGION)")
|
|
258
236
|
@click.option("--format", "format_", type=click.Choice(["json", "csv"]), default="json", help="Output format")
|
|
259
237
|
@click.pass_context
|
|
260
238
|
def export(ctx, service, endpoint, region, format_):
|
|
@@ -10,6 +10,11 @@ def _parse_aws_endpoint_url(environ: Mapping[str, str]) -> str | None:
|
|
|
10
10
|
return environ.get("AWS_ENDPOINT_URL") or None
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def _parse_aws_credential(environ: Mapping[str, str], name: str) -> str | None:
|
|
14
|
+
"""An access key, secret key or session token. Empty counts as unset (``AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}`` in Compose)."""
|
|
15
|
+
return environ.get(name, "").strip() or None
|
|
16
|
+
|
|
17
|
+
|
|
13
18
|
AWS_ENDPOINT_URL: str | None = _parse_aws_endpoint_url(os.environ)
|
|
14
19
|
|
|
15
20
|
# Remove AWS_ENDPOINT_URL from the process environment so boto3's internal
|
|
@@ -17,8 +22,10 @@ AWS_ENDPOINT_URL: str | None = _parse_aws_endpoint_url(os.environ)
|
|
|
17
22
|
if "AWS_ENDPOINT_URL" in os.environ:
|
|
18
23
|
del os.environ["AWS_ENDPOINT_URL"]
|
|
19
24
|
AWS_REGION: str = os.environ.get("AWS_REGION", "us-east-1")
|
|
20
|
-
AWS_ACCESS_KEY_ID: str | None = os.environ
|
|
21
|
-
AWS_SECRET_ACCESS_KEY: str | None = os.environ
|
|
25
|
+
AWS_ACCESS_KEY_ID: str | None = _parse_aws_credential(os.environ, "AWS_ACCESS_KEY_ID")
|
|
26
|
+
AWS_SECRET_ACCESS_KEY: str | None = _parse_aws_credential(os.environ, "AWS_SECRET_ACCESS_KEY")
|
|
27
|
+
# Belongs to the two keys above (SSO exports, assumed roles). Read once at start-up.
|
|
28
|
+
AWS_SESSION_TOKEN: str | None = _parse_aws_credential(os.environ, "AWS_SESSION_TOKEN")
|
|
22
29
|
STACKPORT_ALLOW_WRITES: bool = os.environ.get("STACKPORT_ALLOW_WRITES", "true").lower() in ("1", "true", "yes")
|
|
23
30
|
STACKPORT_PORT: int = int(os.environ.get("STACKPORT_PORT", "8080"))
|
|
24
31
|
# Interface the server listens on. Loopback by default: StackPort has no login
|
|
@@ -8,7 +8,9 @@ Credentials, most specific first:
|
|
|
8
8
|
|
|
9
9
|
1. the endpoint's own auth (a profile, or an access key pair);
|
|
10
10
|
2. AWS Emulator's ``emulator_credentials`` for this endpoint, when present;
|
|
11
|
-
3. ``AWS_ACCESS_KEY_ID`` / ``AWS_SECRET_ACCESS_KEY`` from StackPort's environment
|
|
11
|
+
3. ``AWS_ACCESS_KEY_ID`` / ``AWS_SECRET_ACCESS_KEY`` from StackPort's environment,
|
|
12
|
+
with ``AWS_SESSION_TOKEN`` when it is set (temporary credentials). The token
|
|
13
|
+
belongs to that pair only: no other source below or above ever gets it;
|
|
12
14
|
4. placeholder keys when AWS Emulator's folder exists without keys (it runs
|
|
13
15
|
with ``--auth off``, or it is stopped), so the default chain never sends the
|
|
14
16
|
access key ID of a real account to it, and a stopped emulator shows up as
|
|
@@ -32,6 +34,7 @@ A plan is hashable and carries the emulator files' stat stamp, so
|
|
|
32
34
|
|
|
33
35
|
from __future__ import annotations
|
|
34
36
|
|
|
37
|
+
import hashlib
|
|
35
38
|
import os
|
|
36
39
|
import ssl
|
|
37
40
|
from dataclasses import dataclass, field
|
|
@@ -43,7 +46,7 @@ from urllib3.exceptions import LocationParseError
|
|
|
43
46
|
from urllib3.util import parse_url
|
|
44
47
|
|
|
45
48
|
from backend import aws_emulator
|
|
46
|
-
from backend.config import AWS_ACCESS_KEY_ID, AWS_REGION, AWS_SECRET_ACCESS_KEY, is_local_endpoint
|
|
49
|
+
from backend.config import AWS_ACCESS_KEY_ID, AWS_REGION, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, is_local_endpoint
|
|
47
50
|
|
|
48
51
|
CredentialSource = Literal[
|
|
49
52
|
"endpoint-profile",
|
|
@@ -95,14 +98,21 @@ class EndpointInfo:
|
|
|
95
98
|
"""Prefix for cache keys holding this endpoint's results.
|
|
96
99
|
|
|
97
100
|
Two saved endpoints can share a URL with different auth or TLS settings,
|
|
98
|
-
so all of them are in it
|
|
99
|
-
|
|
101
|
+
so all of them are in it. The secret key is there only as a short
|
|
102
|
+
digest, enough to tell two keys apart and useless for recovering one.
|
|
103
|
+
It starts with the URL, so editing an endpoint, which drops
|
|
104
|
+
``"<url>:"`` keys, drops these too.
|
|
100
105
|
"""
|
|
106
|
+
secret_digest = (
|
|
107
|
+
hashlib.sha256(self.auth_secret_access_key.encode()).hexdigest()[:12]
|
|
108
|
+
if self.auth_secret_access_key
|
|
109
|
+
else None
|
|
110
|
+
)
|
|
101
111
|
return ":".join(
|
|
102
112
|
str(part)
|
|
103
113
|
for part in (
|
|
104
114
|
self.url, self.region, self.auth_type, self.auth_profile,
|
|
105
|
-
self.auth_access_key_id, self.tls_verify, self.ca_bundle,
|
|
115
|
+
self.auth_access_key_id, secret_digest, self.tls_verify, self.ca_bundle,
|
|
106
116
|
)
|
|
107
117
|
)
|
|
108
118
|
|
|
@@ -131,6 +141,7 @@ class ConnectionPlan:
|
|
|
131
141
|
profile: str | None = None
|
|
132
142
|
access_key_id: str | None = field(default=None, repr=False)
|
|
133
143
|
secret_access_key: str | None = field(default=None, repr=False)
|
|
144
|
+
session_token: str | None = field(default=None, repr=False)
|
|
134
145
|
# None leaves verification to botocore (system trust, or AWS_CA_BUNDLE).
|
|
135
146
|
verify: bool | str | None = None
|
|
136
147
|
# A local emulator rather than AWS: see aws_client._build_client for what changes.
|
|
@@ -142,6 +153,15 @@ class InvalidEndpointUrl(ValueError):
|
|
|
142
153
|
"""An endpoint URL StackPort refuses to use."""
|
|
143
154
|
|
|
144
155
|
|
|
156
|
+
def is_direct_endpoint_url(value: str | None) -> bool:
|
|
157
|
+
"""True when an endpoint reference is a URL rather than a saved endpoint's name.
|
|
158
|
+
|
|
159
|
+
The scheme is case-insensitive (``HTTP://host`` is a URL); whether the URL
|
|
160
|
+
is acceptable is ``check_endpoint_url``'s call.
|
|
161
|
+
"""
|
|
162
|
+
return value is not None and value.lower().startswith(("http://", "https://"))
|
|
163
|
+
|
|
164
|
+
|
|
145
165
|
def check_endpoint_url(url: str) -> str:
|
|
146
166
|
"""Return ``url`` if it is a plain http(s) URL every parser reads the same way.
|
|
147
167
|
|
|
@@ -202,7 +222,7 @@ def plan_connection(ep: EndpointInfo) -> ConnectionPlan:
|
|
|
202
222
|
files = aws_emulator.discover(ep.url) if ep.url and not (has_own_credentials and has_own_trust) else None
|
|
203
223
|
|
|
204
224
|
# Credentials
|
|
205
|
-
profile = key = secret = None
|
|
225
|
+
profile = key = secret = token = None
|
|
206
226
|
source: CredentialSource
|
|
207
227
|
if ep.auth_type == "profile" and ep.auth_profile:
|
|
208
228
|
source, profile = "endpoint-profile", ep.auth_profile
|
|
@@ -212,7 +232,7 @@ def plan_connection(ep: EndpointInfo) -> ConnectionPlan:
|
|
|
212
232
|
key, secret = aws_emulator.read_credentials(files.credentials, files.stamp[1])
|
|
213
233
|
source = "aws-emulator"
|
|
214
234
|
elif AWS_ACCESS_KEY_ID is not None or AWS_SECRET_ACCESS_KEY is not None:
|
|
215
|
-
source, key, secret = "environment", AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
|
|
235
|
+
source, key, secret, token = "environment", AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
|
|
216
236
|
elif files is not None:
|
|
217
237
|
source, key, secret = "placeholder", PLACEHOLDER_ACCESS_KEY_ID, PLACEHOLDER_SECRET_ACCESS_KEY
|
|
218
238
|
else:
|
|
@@ -240,6 +260,7 @@ def plan_connection(ep: EndpointInfo) -> ConnectionPlan:
|
|
|
240
260
|
profile=profile,
|
|
241
261
|
access_key_id=key,
|
|
242
262
|
secret_access_key=secret,
|
|
263
|
+
session_token=token,
|
|
243
264
|
verify=verify,
|
|
244
265
|
local_endpoint=is_local_endpoint(ep.url),
|
|
245
266
|
emulator_files=files,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Passing a download from an endpoint on to the browser: S3 objects, Lambda code packages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
from urllib.parse import quote
|
|
9
|
+
|
|
10
|
+
from fastapi.responses import StreamingResponse
|
|
11
|
+
from starlette.types import Receive, Scope, Send
|
|
12
|
+
|
|
13
|
+
_PLAIN_FILENAME = re.compile(r"[A-Za-z0-9._-]+")
|
|
14
|
+
|
|
15
|
+
# How much of the upstream body to read for each chunk sent on. Reading a sync
|
|
16
|
+
# body costs a hop through the thread pool per chunk, so tiny chunks make a
|
|
17
|
+
# download crawl; at this size, relaying keeps pace with reading directly.
|
|
18
|
+
RELAY_CHUNK_BYTES = 64 * 1024
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Closeable(Protocol):
|
|
22
|
+
def close(self) -> None: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def attachment(filename: str) -> str:
|
|
26
|
+
"""A Content-Disposition value that saves the download as ``filename`` (RFC 6266).
|
|
27
|
+
|
|
28
|
+
A name outside a conservative ASCII set gets an underscored fallback plus the
|
|
29
|
+
exact name, percent-encoded as UTF-8, in ``filename*`` (RFC 8187), so quotes,
|
|
30
|
+
backslashes, semicolons, line breaks and characters outside Latin-1 can't
|
|
31
|
+
break the header. The fallback replaces quotes and backslashes rather than
|
|
32
|
+
escaping them, since some browsers don't unescape them (RFC 6266, appendix D).
|
|
33
|
+
"""
|
|
34
|
+
if _PLAIN_FILENAME.fullmatch(filename):
|
|
35
|
+
return f'attachment; filename="{filename}"'
|
|
36
|
+
fallback = re.sub(r"[^A-Za-z0-9._-]", "_", filename)
|
|
37
|
+
return f"attachment; filename=\"{fallback}\"; filename*=UTF-8''{quote(filename, safe='')}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RelayResponse(StreamingResponse):
|
|
41
|
+
"""Streams a body read from an open upstream response, and closes the upstream however the response ends.
|
|
42
|
+
|
|
43
|
+
When the client goes away, Starlette stops iterating the body without
|
|
44
|
+
closing it, so the body iterator can't be the one that closes the upstream.
|
|
45
|
+
An error while reading the upstream propagates, so the server drops the
|
|
46
|
+
connection instead of finishing a cut-off download that would look complete.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
content: Iterable[bytes],
|
|
52
|
+
*,
|
|
53
|
+
upstream: Closeable,
|
|
54
|
+
media_type: str,
|
|
55
|
+
headers: Mapping[str, str],
|
|
56
|
+
):
|
|
57
|
+
super().__init__(content, media_type=media_type, headers=headers)
|
|
58
|
+
self._upstream = upstream
|
|
59
|
+
|
|
60
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
61
|
+
try:
|
|
62
|
+
await super().__call__(scope, receive, send)
|
|
63
|
+
finally:
|
|
64
|
+
self._upstream.close()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Extra shell setup the AWS CLI needs for the endpoint a learner is using.
|
|
2
|
+
|
|
3
|
+
Lessons show commands like ``aws --endpoint-url={{endpointUrl}} s3 ls``. That
|
|
4
|
+
is all MiniStack, Floci, LocalStack and Moto need. Some emulators also want
|
|
5
|
+
environment variables, for example AWS Emulator, which serves HTTPS with a
|
|
6
|
+
certificate and keys it creates at startup. The console steps work without
|
|
7
|
+
any of that because StackPort reads the same files itself, so only the CLI
|
|
8
|
+
path needs the learner to be told.
|
|
9
|
+
|
|
10
|
+
``CLI_SETUP`` maps an emulator id (as reported by ``backend.emulators``) to a
|
|
11
|
+
function returning the setup for it. An emulator that needs nothing has no
|
|
12
|
+
entry, and the lesson shows nothing extra.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import shlex
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from backend import aws_emulator
|
|
23
|
+
from backend.aws_emulator import AwsEmulatorFilesError
|
|
24
|
+
from backend.connection import ConnectionPlan, plan_connection
|
|
25
|
+
from backend.emulators import detect_emulator_quickly
|
|
26
|
+
from backend.routes.common import EndpointInfo
|
|
27
|
+
|
|
28
|
+
# Shell credentials outrank AWS_SHARED_CREDENTIALS_FILE, so leftovers would
|
|
29
|
+
# sign requests with keys the emulator does not know.
|
|
30
|
+
_CLEAR_CREDENTIALS = "unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class CliSetup:
|
|
35
|
+
"""Shell lines to run once, and an optional sentence about where they come from."""
|
|
36
|
+
|
|
37
|
+
lines: list[str]
|
|
38
|
+
note: str | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def running_in_container() -> bool:
|
|
42
|
+
"""True inside Docker or Podman, where StackPort's filesystem is not the learner's."""
|
|
43
|
+
return Path("/.dockerenv").exists() or Path("/run/.containerenv").exists()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _shell_path(path: Path) -> str:
|
|
47
|
+
"""Render a path for pasting into a shell, keeping ``~`` for the home folder."""
|
|
48
|
+
shown = aws_emulator.display_path(path)
|
|
49
|
+
if not shown.startswith("~/"):
|
|
50
|
+
return shlex.quote(shown)
|
|
51
|
+
rest = shown[2:]
|
|
52
|
+
quoted = shlex.quote(rest)
|
|
53
|
+
return f"~/{rest}" if quoted == rest else f'"$HOME"/{quoted}'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _aws_emulator_setup(ep: EndpointInfo, plan: ConnectionPlan) -> CliSetup | None:
|
|
57
|
+
# Only when StackPort itself uses the emulator's files: an endpoint with its
|
|
58
|
+
# own keys or CA bundle is not one the CLI needs them for.
|
|
59
|
+
if "aws-emulator" not in (plan.credential_source, plan.trust_source):
|
|
60
|
+
return None
|
|
61
|
+
directory = aws_emulator.files_dir_for(ep.url)
|
|
62
|
+
if directory is None:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
note = None
|
|
66
|
+
if running_in_container():
|
|
67
|
+
# The folder StackPort sees is inside its container. AWS Emulator's own
|
|
68
|
+
# guide puts the files in the home folder of the machine running it.
|
|
69
|
+
port = directory.name
|
|
70
|
+
directory = Path.home() / ".aws" / "emulator" / port
|
|
71
|
+
note = (
|
|
72
|
+
"StackPort runs in a container, so these are the paths on your machine, where AWS Emulator "
|
|
73
|
+
"keeps its files when it runs there. With the Compose example the files live in the emulator's "
|
|
74
|
+
"volume: copy them out with `mkdir -p ~/.aws/emulator && docker compose -f "
|
|
75
|
+
f"docker-compose.aws-emulator.yml cp aws-emulator:/home/emulator/.aws/emulator/{port} "
|
|
76
|
+
"~/.aws/emulator/`, again after each restart."
|
|
77
|
+
)
|
|
78
|
+
return CliSetup(
|
|
79
|
+
lines=[
|
|
80
|
+
_CLEAR_CREDENTIALS,
|
|
81
|
+
f"export AWS_SHARED_CREDENTIALS_FILE={_shell_path(directory / aws_emulator.CREDENTIALS_FILE)}",
|
|
82
|
+
f"export AWS_CA_BUNDLE={_shell_path(directory / aws_emulator.CA_FILE)}",
|
|
83
|
+
# The emulator serves every service on one host, so the CLI must not
|
|
84
|
+
# prepend a service name to it (for example "s3-control.")
|
|
85
|
+
"export AWS_DISABLE_HOST_PREFIX_INJECTION=true",
|
|
86
|
+
],
|
|
87
|
+
note=note,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
CLI_SETUP: dict[str, Callable[[EndpointInfo, ConnectionPlan], CliSetup | None]] = {
|
|
92
|
+
"aws-emulator": _aws_emulator_setup,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cli_setup_for(ep: EndpointInfo) -> dict | None:
|
|
97
|
+
"""The setup block for this endpoint, or None when the plain command works.
|
|
98
|
+
|
|
99
|
+
``{"emulator": <display name>, "script": <shell lines>, "note": <sentence or None>}``.
|
|
100
|
+
Never probes an endpoint that is not saved, and waits only briefly for an
|
|
101
|
+
emulator that has not been identified yet, so the UI asks again later.
|
|
102
|
+
"""
|
|
103
|
+
emulator = detect_emulator_quickly(ep)
|
|
104
|
+
builder = CLI_SETUP.get(emulator["id"]) if emulator else None
|
|
105
|
+
if builder is None:
|
|
106
|
+
return None
|
|
107
|
+
try:
|
|
108
|
+
plan = plan_connection(ep)
|
|
109
|
+
except AwsEmulatorFilesError:
|
|
110
|
+
return None
|
|
111
|
+
setup = builder(ep, plan)
|
|
112
|
+
if setup is None:
|
|
113
|
+
return None
|
|
114
|
+
return {"emulator": emulator["name"], "script": "\n".join(setup.lines), "note": setup.note}
|
|
@@ -98,7 +98,7 @@ def _dynamodb_item_count(ep: EndpointInfo, params: dict) -> dict:
|
|
|
98
98
|
|
|
99
99
|
def _resource_exists(ep: EndpointInfo, params: dict) -> dict:
|
|
100
100
|
"""Generic existence check driven by SERVICE_REGISTRY (no cache)."""
|
|
101
|
-
from backend.routes.stats import
|
|
101
|
+
from backend.routes.stats import SERVICE_REGISTRY, list_items
|
|
102
102
|
|
|
103
103
|
service = params["service"]
|
|
104
104
|
resource_type = params["resource_type"]
|
|
@@ -108,12 +108,7 @@ def _resource_exists(ep: EndpointInfo, params: dict) -> dict:
|
|
|
108
108
|
if rtype != resource_type:
|
|
109
109
|
continue
|
|
110
110
|
client = get_client(boto_service, **ep.client_kwargs())
|
|
111
|
-
|
|
112
|
-
response = getattr(client, method)(**kwargs)
|
|
113
|
-
items = response.get(response_key, [])
|
|
114
|
-
if isinstance(items, dict) and "Items" in items:
|
|
115
|
-
items = items["Items"]
|
|
116
|
-
for item in items:
|
|
111
|
+
for item in list_items(client, boto_service, method, response_key):
|
|
117
112
|
if isinstance(item, str) and target in item:
|
|
118
113
|
return {"passed": True, "message": f"Found {resource_type[:-1] if resource_type.endswith('s') else resource_type} '{target}'."}
|
|
119
114
|
if isinstance(item, dict) and any(target in str(v) for v in item.values() if isinstance(v, str)):
|