stackport 0.4.7__tar.gz → 0.4.8__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.8}/PKG-INFO +3 -2
- {stackport-0.4.7 → stackport-0.4.8}/README.md +2 -1
- {stackport-0.4.7 → stackport-0.4.8}/backend/aws_client.py +2 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/cli.py +45 -16
- {stackport-0.4.7 → stackport-0.4.8}/backend/config.py +9 -2
- {stackport-0.4.7 → stackport-0.4.8}/backend/connection.py +28 -7
- stackport-0.4.8/backend/learn/cli_setup.py +114 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/common.py +22 -20
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/endpoints.py +3 -2
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/learn.py +12 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/websocket.py +125 -50
- {stackport-0.4.7 → stackport-0.4.8}/pyproject.toml +1 -1
- {stackport-0.4.7 → stackport-0.4.8/stackport.egg-info}/PKG-INFO +3 -2
- {stackport-0.4.7 → stackport-0.4.8}/stackport.egg-info/SOURCES.txt +28 -26
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_aws_client_tls.py +43 -0
- stackport-0.4.8/tests/test_cli.py +370 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_connection.py +95 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_endpoint_store_env.py +10 -0
- stackport-0.4.8/tests/test_learn_cli_setup.py +153 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_multi_endpoint.py +12 -8
- stackport-0.4.8/tests/test_websocket.py +409 -0
- stackport-0.4.7/ui/dist/assets/CloudscapeAbout-C464xiLP.js → stackport-0.4.8/ui/dist/assets/CloudscapeAbout-ykzJ5x3Y.js +1 -1
- stackport-0.4.7/ui/dist/assets/CloudscapeDashboard--Puj8I6B.js → stackport-0.4.8/ui/dist/assets/CloudscapeDashboard-CJt6f1T3.js +1 -1
- stackport-0.4.7/ui/dist/assets/CloudscapeLearn-CT7ucW_g.js → stackport-0.4.8/ui/dist/assets/CloudscapeLearn-l4d6rjUk.js +1 -1
- stackport-0.4.7/ui/dist/assets/CloudscapeResourceBrowser-CprDqhcQ.js → stackport-0.4.8/ui/dist/assets/CloudscapeResourceBrowser-DETT9g8r.js +3 -3
- stackport-0.4.7/ui/dist/assets/CloudscapeSettings-D_9KUPvP.js → stackport-0.4.8/ui/dist/assets/CloudscapeSettings-Hg6Upkx0.js +1 -1
- stackport-0.4.7/ui/dist/assets/CloudscapeShell-BCyqqtsn.js → stackport-0.4.8/ui/dist/assets/CloudscapeShell-S38ixn79.js +1 -1
- stackport-0.4.7/ui/dist/assets/ServiceStatus-KR6X2qeu.js → stackport-0.4.8/ui/dist/assets/ServiceStatus-BHSv7SA3.js +1 -1
- stackport-0.4.7/ui/dist/assets/StateMachineGraph-D9IP3035.js → stackport-0.4.8/ui/dist/assets/StateMachineGraph-DUiGayyA.js +1 -1
- stackport-0.4.8/ui/dist/assets/index-CTGN6ph7.js +67 -0
- stackport-0.4.7/ui/dist/assets/index-WR8pyfQO.js → stackport-0.4.8/ui/dist/assets/index-ClXofh-m.js +1 -1
- stackport-0.4.7/ui/dist/assets/index-DKnYLurL.js → stackport-0.4.8/ui/dist/assets/index-DEgNFpXr.js +1 -1
- stackport-0.4.7/ui/dist/assets/index-WNsJzp13.js → stackport-0.4.8/ui/dist/assets/index-SWJZXC-H.js +1 -1
- stackport-0.4.7/ui/dist/assets/index-CHdSsqYw.js → stackport-0.4.8/ui/dist/assets/index-v1JyxN8N.js +1 -1
- {stackport-0.4.7 → stackport-0.4.8}/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/index-CvtAnA9F.js +0 -67
- {stackport-0.4.7 → stackport-0.4.8}/LICENSE +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/MANIFEST.in +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/__init__.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/aws_emulator.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/aws_errors.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/cache.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/emulators.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/endpoint_store.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/error_handlers.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/learn/__init__.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/learn/content.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/learn/hotspots.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/learn/trails/aws-first-steps.json +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/learn/verify.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/learn_store.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/main.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/origins.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/__init__.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/dynamodb.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/ec2.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/iam.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/kms.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/lambda_svc.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/logs.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/monitoring.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/rds.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/resources.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/s3.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/secretsmanager.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/sns.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/sqs.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/stats.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/stepfunctions.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/routes/tags.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/__init__.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/dynamodb.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/endpoints.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/lambda_svc.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/learn.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/logs.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/monitoring.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/s3.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/secretsmanager.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/sns.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/sqs.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/stepfunctions.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/backend/schemas/tags.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/setup.cfg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/stackport.egg-info/requires.txt +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/stackport.egg-info/top_level.txt +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_aws_emulator.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_aws_errors.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_cache.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_client.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_config.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_create_resources.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_dynamodb_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_ec2_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_emulators.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_endpoint_store.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_endpoints.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_endpoints_tls.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_error_handlers.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_iam_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_kms_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_lambda_code_download.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_lambda_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_learn_content.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_learn_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_learn_store.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_logs_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_logs_tail.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_monitoring_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_rds_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_readonly_middleware.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_registries.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_request_safety.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_s3_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_s3_upload_limit_env.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_secretsmanager_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_sns_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_sqs_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_stats_probe.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/tests/test_tags_routes.py +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/assets/CloudscapeResourceBrowser-BR6m7Ajg.css +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/assets/CloudscapeSettings-DI0pOcAA.css +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/assets/CloudscapeShell-oIMuVycJ.css +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/assets/ServiceStatus-DNBdeKId.css +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/assets/index-DKIdXFX0.css +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/assets/index-D__irddE.css +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/assets/index-DoY0HscW.css +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/assets/index-RwoKZF4y.css +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/assets/utils-Dm4tJbJR.js +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/acm.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/apigateway.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/appsync.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/athena.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/cloudformation.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/cloudfront.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/cognito-idp.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/dynamodb.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/ec2.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/ecr.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/ecs.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/elasticache.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/events.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/firehose.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/glue.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/iam.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/kinesis.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/kms.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/lambda.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/logs.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/monitoring.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/rds.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/route53.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/s3.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/secretsmanager.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/ses.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/sns.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/sqs.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/ssm.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/stepfunctions.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/aws-icons/wafv2.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/favicon.png +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/ui/dist/favicon.svg +0 -0
- {stackport-0.4.7 → stackport-0.4.8}/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.8
|
|
4
4
|
Summary: Universal AWS resource browser for local emulators
|
|
5
5
|
Author: Davi Reis Vieira
|
|
6
6
|
License: MIT
|
|
@@ -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 |
|
|
@@ -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 |
|
|
@@ -47,6 +47,8 @@ def _build_client(service_name: str, plan: ConnectionPlan):
|
|
|
47
47
|
session_kwargs["aws_access_key_id"] = plan.access_key_id
|
|
48
48
|
if plan.secret_access_key is not None:
|
|
49
49
|
session_kwargs["aws_secret_access_key"] = plan.secret_access_key
|
|
50
|
+
if plan.session_token is not None:
|
|
51
|
+
session_kwargs["aws_session_token"] = plan.session_token
|
|
50
52
|
session = boto3.Session(**session_kwargs)
|
|
51
53
|
|
|
52
54
|
client_kwargs: dict = {"service_name": service_name}
|
|
@@ -12,13 +12,15 @@ 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
25
|
DESCRIBE_REGISTRY,
|
|
24
26
|
_PREFERRED_ID_FIELD,
|
|
@@ -30,16 +32,43 @@ from backend.routes.stats import SERVICE_REGISTRY, _METHOD_KWARGS, _count_items,
|
|
|
30
32
|
logger = logging.getLogger(__name__)
|
|
31
33
|
|
|
32
34
|
|
|
35
|
+
def _startup_endpoint_info() -> EndpointInfo:
|
|
36
|
+
"""Connection info for AWS_ENDPOINT_URL, with the settings of the env endpoint it defines.
|
|
37
|
+
|
|
38
|
+
The store points that env endpoint (normally "default") at the environment's URL
|
|
39
|
+
for the run. If it was deleted, the URL is used on its own.
|
|
40
|
+
"""
|
|
41
|
+
name = next((n for n, url in ENDPOINTS.items() if url == AWS_ENDPOINT_URL), None)
|
|
42
|
+
entry = endpoint_store.get(name) if name is not None else None
|
|
43
|
+
if entry is not None and entry["source"] == "env" and entry["url"] == AWS_ENDPOINT_URL:
|
|
44
|
+
return EndpointInfo.from_entry(entry)
|
|
45
|
+
return endpoint_info_for(AWS_ENDPOINT_URL)
|
|
46
|
+
|
|
47
|
+
|
|
33
48
|
def _client_kwargs(endpoint: str | None, region: str | None) -> dict:
|
|
34
|
-
"""Resolve where a CLI command talks to.
|
|
49
|
+
"""Resolve where a CLI command talks to, the way the web app does.
|
|
35
50
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
``endpoint`` is a saved endpoint's name or a URL. Without it, AWS_ENDPOINT_URL
|
|
52
|
+
at startup wins (with the settings of the env endpoint that defines it), then
|
|
53
|
+
the saved default endpoint. A saved endpoint brings its auth and TLS settings
|
|
54
|
+
along; an explicit ``region`` overrides its region. Passing the result to
|
|
55
|
+
get_client explicitly is what makes the flag work once
|
|
56
|
+
~/.stackport/endpoints.json exists.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
click.BadParameter: the name matches no saved endpoint, or the URL is refused.
|
|
40
60
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
try:
|
|
62
|
+
if endpoint is None and AWS_ENDPOINT_URL is not None:
|
|
63
|
+
info = _startup_endpoint_info()
|
|
64
|
+
else:
|
|
65
|
+
info = endpoint_info_for(endpoint, strict=True)
|
|
66
|
+
except (InvalidEndpointUrl, UnknownEndpoint) as exc:
|
|
67
|
+
raise click.BadParameter(str(exc), param_hint="--endpoint") from exc
|
|
68
|
+
kwargs = info.client_kwargs()
|
|
69
|
+
if region is not None:
|
|
70
|
+
kwargs["region"] = region
|
|
71
|
+
return kwargs
|
|
43
72
|
|
|
44
73
|
|
|
45
74
|
@click.group(invoke_without_command=True)
|
|
@@ -60,8 +89,8 @@ def serve(port, host):
|
|
|
60
89
|
|
|
61
90
|
|
|
62
91
|
@cli.command()
|
|
63
|
-
@click.option("--endpoint", default=
|
|
64
|
-
@click.option("--region", default=
|
|
92
|
+
@click.option("--endpoint", default=None, help="Saved endpoint name or endpoint URL (default: AWS_ENDPOINT_URL if set, else the saved default endpoint)")
|
|
93
|
+
@click.option("--region", default=None, help="AWS region (default: the endpoint's region, then AWS_REGION)")
|
|
65
94
|
@click.option("--output", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
66
95
|
def status(endpoint, region, output):
|
|
67
96
|
"""Show all services with availability and resource counts."""
|
|
@@ -102,8 +131,8 @@ def status(endpoint, region, output):
|
|
|
102
131
|
|
|
103
132
|
@cli.command()
|
|
104
133
|
@click.argument("service")
|
|
105
|
-
@click.option("--endpoint", default=
|
|
106
|
-
@click.option("--region", default=
|
|
134
|
+
@click.option("--endpoint", default=None, help="Saved endpoint name or endpoint URL (default: AWS_ENDPOINT_URL if set, else the saved default endpoint)")
|
|
135
|
+
@click.option("--region", default=None, help="AWS region (default: the endpoint's region, then AWS_REGION)")
|
|
107
136
|
@click.option("--output", type=click.Choice(["json", "table", "csv"]), default="table", help="Output format")
|
|
108
137
|
def list(service, endpoint, region, output):
|
|
109
138
|
"""List resources for a service."""
|
|
@@ -171,8 +200,8 @@ def list(service, endpoint, region, output):
|
|
|
171
200
|
@click.argument("service")
|
|
172
201
|
@click.argument("resource_type")
|
|
173
202
|
@click.argument("resource_id")
|
|
174
|
-
@click.option("--endpoint", default=
|
|
175
|
-
@click.option("--region", default=
|
|
203
|
+
@click.option("--endpoint", default=None, help="Saved endpoint name or endpoint URL (default: AWS_ENDPOINT_URL if set, else the saved default endpoint)")
|
|
204
|
+
@click.option("--region", default=None, help="AWS region (default: the endpoint's region, then AWS_REGION)")
|
|
176
205
|
@click.option("--output", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
177
206
|
def describe(service, resource_type, resource_id, endpoint, region, output):
|
|
178
207
|
"""Describe a specific resource."""
|
|
@@ -253,8 +282,8 @@ def describe(service, resource_type, resource_id, endpoint, region, output):
|
|
|
253
282
|
|
|
254
283
|
@cli.command()
|
|
255
284
|
@click.argument("service")
|
|
256
|
-
@click.option("--endpoint", default=
|
|
257
|
-
@click.option("--region", default=
|
|
285
|
+
@click.option("--endpoint", default=None, help="Saved endpoint name or endpoint URL (default: AWS_ENDPOINT_URL if set, else the saved default endpoint)")
|
|
286
|
+
@click.option("--region", default=None, help="AWS region (default: the endpoint's region, then AWS_REGION)")
|
|
258
287
|
@click.option("--format", "format_", type=click.Choice(["json", "csv"]), default="json", help="Output format")
|
|
259
288
|
@click.pass_context
|
|
260
289
|
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,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}
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
from fastapi import HTTPException, Query
|
|
4
4
|
|
|
5
5
|
from backend.config import endpoint_store
|
|
6
|
-
from backend.connection import EndpointInfo, InvalidEndpointUrl, check_endpoint_url
|
|
6
|
+
from backend.connection import EndpointInfo, InvalidEndpointUrl, check_endpoint_url, is_direct_endpoint_url
|
|
7
7
|
|
|
8
|
-
__all__ = [
|
|
8
|
+
__all__ = [
|
|
9
|
+
"EndpointInfo",
|
|
10
|
+
"UnknownEndpoint",
|
|
11
|
+
"endpoint_info_for",
|
|
12
|
+
"get_endpoint_info",
|
|
13
|
+
"get_endpoint_url",
|
|
14
|
+
]
|
|
9
15
|
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
class UnknownEndpoint(LookupError):
|
|
18
|
+
"""A name that matches no saved endpoint."""
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
def get_endpoint_url(endpoint: str | None = Query(None, description="Endpoint name or URL")) -> str | None:
|
|
@@ -21,22 +27,29 @@ def get_endpoint_url(endpoint: str | None = Query(None, description="Endpoint na
|
|
|
21
27
|
Returns:
|
|
22
28
|
Endpoint URL to use for AWS API calls, or None for real AWS.
|
|
23
29
|
"""
|
|
24
|
-
if
|
|
30
|
+
if is_direct_endpoint_url(endpoint):
|
|
25
31
|
try:
|
|
26
|
-
check_endpoint_url(endpoint)
|
|
32
|
+
return check_endpoint_url(endpoint)
|
|
27
33
|
except InvalidEndpointUrl as exc:
|
|
28
34
|
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
29
35
|
return endpoint_store.resolve(endpoint)
|
|
30
36
|
|
|
31
37
|
|
|
32
|
-
def endpoint_info_for(endpoint: str | None) -> EndpointInfo:
|
|
38
|
+
def endpoint_info_for(endpoint: str | None, strict: bool = False) -> EndpointInfo:
|
|
33
39
|
"""Resolve an endpoint name, a direct URL, or None (the default) to connection info.
|
|
34
40
|
|
|
41
|
+
An unknown name falls back to the default endpoint, which suits HTTP routes;
|
|
42
|
+
``strict`` raises instead, for callers where a typo shouldn't pick another endpoint.
|
|
43
|
+
|
|
35
44
|
Raises:
|
|
36
45
|
InvalidEndpointUrl: a direct URL StackPort refuses to use.
|
|
46
|
+
UnknownEndpoint: ``strict`` and the name matches no saved endpoint.
|
|
37
47
|
"""
|
|
38
|
-
if
|
|
39
|
-
check_endpoint_url(endpoint)
|
|
48
|
+
if is_direct_endpoint_url(endpoint):
|
|
49
|
+
return EndpointInfo(url=check_endpoint_url(endpoint), region=None)
|
|
50
|
+
if strict and endpoint is not None and endpoint_store.get(endpoint) is None:
|
|
51
|
+
known = ", ".join(sorted(endpoint_store.list_all())) or "none"
|
|
52
|
+
raise UnknownEndpoint(f"'{endpoint}' is not a saved endpoint name or an http(s) URL. Saved endpoints: {known}.")
|
|
40
53
|
entry = endpoint_store.resolve_full(endpoint)
|
|
41
54
|
if entry:
|
|
42
55
|
return EndpointInfo.from_entry(entry)
|
|
@@ -45,17 +58,6 @@ def endpoint_info_for(endpoint: str | None) -> EndpointInfo:
|
|
|
45
58
|
return EndpointInfo(url=url, region=region)
|
|
46
59
|
|
|
47
60
|
|
|
48
|
-
def endpoint_info_for_url(url: str | None) -> EndpointInfo:
|
|
49
|
-
"""Connection info for a URL, using the first saved endpoint with that URL, if any.
|
|
50
|
-
|
|
51
|
-
For callers that only kept the URL, such as the WebSocket probe loop.
|
|
52
|
-
"""
|
|
53
|
-
for entry in endpoint_store.list_all().values():
|
|
54
|
-
if entry["url"] == url:
|
|
55
|
-
return EndpointInfo.from_entry(entry)
|
|
56
|
-
return EndpointInfo(url=url, region=None)
|
|
57
|
-
|
|
58
|
-
|
|
59
61
|
def get_endpoint_info(endpoint: str | None = Query(None, description="Endpoint name or URL")) -> EndpointInfo:
|
|
60
62
|
"""Resolve endpoint query param to URL, region, auth, and TLS config.
|
|
61
63
|
|
|
@@ -190,8 +190,9 @@ async def update_endpoint(name: str, body: UpdateEndpointBody):
|
|
|
190
190
|
if old_url:
|
|
191
191
|
cache.delete_by_prefix(f"{old_url}:")
|
|
192
192
|
|
|
193
|
-
# Broadcast change to connected clients
|
|
194
|
-
from backend.websocket import broadcast_endpoints_changed
|
|
193
|
+
# Broadcast change to connected clients; live stats kept under the old settings go too
|
|
194
|
+
from backend.websocket import broadcast_endpoints_changed, remove_endpoint_from_stats
|
|
195
|
+
remove_endpoint_from_stats(old_url)
|
|
195
196
|
asyncio.create_task(broadcast_endpoints_changed())
|
|
196
197
|
|
|
197
198
|
return {"name": name, **_public_entry(entry)}
|
|
@@ -19,6 +19,7 @@ from typing import Any
|
|
|
19
19
|
from fastapi import APIRouter, Depends, HTTPException
|
|
20
20
|
|
|
21
21
|
from backend.config import STACKPORT_DATA_DIR
|
|
22
|
+
from backend.learn.cli_setup import cli_setup_for
|
|
22
23
|
from backend.learn.verify import run_check
|
|
23
24
|
from backend.learn_store import LearnProgressStore
|
|
24
25
|
from backend.routes.common import EndpointInfo, get_endpoint_info
|
|
@@ -155,6 +156,17 @@ def get_trail(trail_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> d
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
|
|
159
|
+
@router.get("/cli-setup")
|
|
160
|
+
def get_cli_setup(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
|
|
161
|
+
"""What the CLI needs for this endpoint beyond ``--endpoint-url``, or null.
|
|
162
|
+
|
|
163
|
+
Separate from the trail because it can change without the trail changing:
|
|
164
|
+
an emulator is identified in the background, so the first answer may be
|
|
165
|
+
empty and the UI asks again.
|
|
166
|
+
"""
|
|
167
|
+
return {"cliSetup": cli_setup_for(ep)}
|
|
168
|
+
|
|
169
|
+
|
|
158
170
|
def _progress_payload(trail_id: str, progress: dict) -> dict[str, Any]:
|
|
159
171
|
return {
|
|
160
172
|
"completed": progress["completed"].get(trail_id, {}),
|