stackport 0.4.1__tar.gz → 0.4.3__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.3/MANIFEST.in +2 -0
- {stackport-0.4.1/stackport.egg-info → stackport-0.4.3}/PKG-INFO +50 -9
- {stackport-0.4.1 → stackport-0.4.3}/README.md +48 -7
- {stackport-0.4.1 → stackport-0.4.3}/backend/cli.py +37 -35
- {stackport-0.4.1 → stackport-0.4.3}/backend/config.py +3 -0
- stackport-0.4.3/backend/learn/__init__.py +1 -0
- stackport-0.4.3/backend/learn/content.py +107 -0
- stackport-0.4.3/backend/learn/hotspots.py +27 -0
- stackport-0.4.3/backend/learn/trails/aws-first-steps.json +171 -0
- stackport-0.4.3/backend/learn/verify.py +172 -0
- stackport-0.4.3/backend/learn_store.py +130 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/main.py +10 -2
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/dynamodb.py +55 -1
- stackport-0.4.3/backend/routes/learn.py +233 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/resources.py +1 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/s3.py +43 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/stats.py +2 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/dynamodb.py +22 -0
- stackport-0.4.3/backend/schemas/learn.py +44 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/s3.py +51 -1
- {stackport-0.4.1 → stackport-0.4.3}/pyproject.toml +3 -2
- {stackport-0.4.1 → stackport-0.4.3/stackport.egg-info}/PKG-INFO +50 -9
- {stackport-0.4.1 → stackport-0.4.3}/stackport.egg-info/SOURCES.txt +52 -36
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_cli.py +65 -0
- stackport-0.4.3/tests/test_create_resources.py +238 -0
- stackport-0.4.3/tests/test_learn_content.py +241 -0
- stackport-0.4.3/tests/test_learn_routes.py +284 -0
- stackport-0.4.3/tests/test_learn_store.py +52 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_registries.py +17 -1
- stackport-0.4.3/ui/dist/assets/CloudscapeAbout-BRbsDskR.js +1 -0
- stackport-0.4.3/ui/dist/assets/CloudscapeDashboard-j2f4Elz2.js +1 -0
- stackport-0.4.3/ui/dist/assets/CloudscapeLearn-CVW3k3X0.js +1 -0
- stackport-0.4.3/ui/dist/assets/CloudscapeResourceBrowser-CgWdgn20.js +19 -0
- stackport-0.4.1/ui/dist/assets/CloudscapeResourceBrowser-DNa6P8a7.css → stackport-0.4.3/ui/dist/assets/CloudscapeResourceBrowser-CzwePG-V.css +1 -1
- stackport-0.4.3/ui/dist/assets/CloudscapeSettings-CIXFvPKv.js +1 -0
- stackport-0.4.3/ui/dist/assets/CloudscapeShell-BXHVWjDE.css +1 -0
- stackport-0.4.3/ui/dist/assets/CloudscapeShell-DNfhSg0O.js +195 -0
- stackport-0.4.1/ui/dist/assets/StateMachineGraph-wwDpbmDc.js → stackport-0.4.3/ui/dist/assets/StateMachineGraph-DvF_kIOO.js +1 -1
- stackport-0.4.3/ui/dist/assets/index-BC7E-Fm4.js +1 -0
- stackport-0.4.3/ui/dist/assets/index-BLdRlH9d.js +67 -0
- stackport-0.4.3/ui/dist/assets/index-Ba-oOcd3.js +1 -0
- stackport-0.4.3/ui/dist/assets/index-BkSgVQ0X.js +1 -0
- stackport-0.4.3/ui/dist/assets/index-C5z3cir-.js +1 -0
- stackport-0.4.3/ui/dist/assets/index-DKIdXFX0.css +1 -0
- stackport-0.4.1/ui/dist/assets/index-DaXIyw62.css → stackport-0.4.3/ui/dist/assets/index-DNBdeKId.css +1 -1
- stackport-0.4.3/ui/dist/assets/index-DoY0HscW.css +1 -0
- stackport-0.4.3/ui/dist/assets/index-DrkkXAoG.css +1 -0
- stackport-0.4.3/ui/dist/assets/index-Dw6TGweb.js +1 -0
- stackport-0.4.3/ui/dist/assets/index-Vf0UFiCT.css +1 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/index.html +2 -2
- stackport-0.4.1/MANIFEST.in +0 -1
- stackport-0.4.1/ui/dist/assets/CloudscapeAbout-BFh7x4BC.js +0 -1
- stackport-0.4.1/ui/dist/assets/CloudscapeDashboard-DzRbBwTo.js +0 -1
- stackport-0.4.1/ui/dist/assets/CloudscapeResourceBrowser-DZE3EncJ.js +0 -19
- stackport-0.4.1/ui/dist/assets/CloudscapeSettings-BOzr9FgE.js +0 -1
- stackport-0.4.1/ui/dist/assets/CloudscapeShell-C1R1SIYZ.css +0 -1
- stackport-0.4.1/ui/dist/assets/CloudscapeShell-KCrV2gOE.js +0 -201
- stackport-0.4.1/ui/dist/assets/index-BKtAPb3s.js +0 -1
- stackport-0.4.1/ui/dist/assets/index-BVHxAbst.js +0 -1
- stackport-0.4.1/ui/dist/assets/index-C_MN8WBt.js +0 -1
- stackport-0.4.1/ui/dist/assets/index-Cah5DqCf.css +0 -1
- stackport-0.4.1/ui/dist/assets/index-D7KAiUgR.js +0 -52
- stackport-0.4.1/ui/dist/assets/index-DRZKHoZl.css +0 -1
- stackport-0.4.1/ui/dist/assets/index-F7IxH4zH.js +0 -1
- stackport-0.4.1/ui/dist/assets/index-bpZOyX6N.js +0 -1
- stackport-0.4.1/ui/dist/assets/index-siFAyqi4.css +0 -1
- {stackport-0.4.1 → stackport-0.4.3}/LICENSE +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/__init__.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/aws_client.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/cache.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/endpoint_store.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/__init__.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/common.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/ec2.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/endpoints.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/iam.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/lambda_svc.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/logs.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/monitoring.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/rds.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/secretsmanager.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/sns.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/sqs.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/stepfunctions.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/routes/tags.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/__init__.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/endpoints.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/lambda_svc.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/logs.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/monitoring.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/secretsmanager.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/sns.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/sqs.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/stepfunctions.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/schemas/tags.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/backend/websocket.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/setup.cfg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/stackport.egg-info/requires.txt +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/stackport.egg-info/top_level.txt +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_cache.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_client.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_config.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_dynamodb_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_ec2_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_endpoint_store.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_endpoints.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_iam_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_lambda_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_logs_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_logs_tail.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_monitoring_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_multi_endpoint.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_rds_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_readonly_middleware.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_s3_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_s3_upload_limit_env.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_secretsmanager_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_sns_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_sqs_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_tags_routes.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/tests/test_websocket.py +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/assets/index-DE--A2EE.css +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/assets/utils-Cfmwk2UT.js +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/acm.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/apigateway.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/appsync.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/athena.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/cloudformation.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/cloudfront.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/cognito-idp.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/dynamodb.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/ec2.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/ecr.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/ecs.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/elasticache.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/events.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/firehose.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/glue.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/iam.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/kinesis.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/kms.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/lambda.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/logs.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/monitoring.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/rds.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/route53.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/s3.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/secretsmanager.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/ses.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/sns.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/sqs.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/ssm.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/stepfunctions.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/aws-icons/wafv2.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/favicon.png +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/favicon.svg +0 -0
- {stackport-0.4.1 → stackport-0.4.3}/ui/dist/stackport-logo.svg +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackport
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Universal AWS resource browser for local emulators
|
|
5
5
|
Author: Davi Reis Vieira
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/DaviReisVieira/stackport
|
|
8
8
|
Project-URL: Repository, https://github.com/DaviReisVieira/stackport
|
|
9
9
|
Project-URL: Issues, https://github.com/DaviReisVieira/stackport/issues
|
|
10
|
-
Keywords: aws,ministack,localstack,browser,devtools,emulator
|
|
10
|
+
Keywords: aws,ministack,floci,localstack,browser,devtools,emulator
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Framework :: FastAPI
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -70,19 +70,23 @@ Dynamic: license-file
|
|
|
70
70
|
- **Write operations** — upload/delete S3 objects, query DynamoDB, invoke Lambda, send/receive SQS messages
|
|
71
71
|
- **Real AWS support** — connect to real AWS accounts with read-only mode by default
|
|
72
72
|
- **Per-endpoint authentication** — default credentials, AWS profiles (SSO/AssumeRole), or static keys per endpoint
|
|
73
|
+
- **Guided lessons** — hands-on AWS tutorials that run inside the console and verify your real resource state
|
|
73
74
|
- **Tag management** — unified tagging across 21 resource types
|
|
74
75
|
- **CLI** — `stackport status`, `list`, `describe`, `export` with JSON/CSV/table output
|
|
75
76
|
- **Real-time dashboard** with WebSocket-powered live updates
|
|
76
77
|
- **Keyboard shortcuts** — 16 shortcuts for fast navigation (press `?` to view)
|
|
77
|
-
- Single Docker image, works with MiniStack, LocalStack, Moto, or any AWS-compatible endpoint
|
|
78
|
+
- Single Docker image, works with [MiniStack](https://github.com/ministackorg/ministack), [Floci](https://github.com/floci-io/floci), LocalStack, Moto, or any AWS-compatible endpoint
|
|
78
79
|
|
|
79
80
|
## Quick Start
|
|
80
81
|
|
|
81
|
-
### With a local emulator
|
|
82
|
+
### With a local emulator
|
|
83
|
+
|
|
84
|
+
Start whichever emulator you use, then point StackPort at it:
|
|
82
85
|
|
|
83
86
|
```bash
|
|
84
|
-
# Start
|
|
85
|
-
|
|
87
|
+
# Start an emulator on :4566 (pick one)
|
|
88
|
+
docker run -d -p 4566:4566 ministackorg/ministack # MiniStack
|
|
89
|
+
docker run -d -p 4566:4566 floci/floci # Floci
|
|
86
90
|
|
|
87
91
|
# Start StackPort
|
|
88
92
|
pip install stackport
|
|
@@ -107,16 +111,32 @@ You can also configure per-endpoint authentication from the Settings UI — sele
|
|
|
107
111
|
|
|
108
112
|
When connected to real AWS, StackPort shows a warning banner and operates in read-only mode unless writes are explicitly enabled.
|
|
109
113
|
|
|
110
|
-
### Docker Compose
|
|
114
|
+
### Docker Compose
|
|
115
|
+
|
|
116
|
+
Each example runs the emulator, StackPort, and a one-shot `seed` container that creates a few resources (S3, SQS, SNS, DynamoDB, Secrets Manager, IAM, Logs, Step Functions) so the dashboard has something to show on first load. Both use the same seed. Swap in LocalStack, Moto, or any AWS-compatible endpoint by replacing the emulator service and updating `AWS_ENDPOINT_URL`.
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
**MiniStack + StackPort** — [`examples/docker-compose.yml`](examples/docker-compose.yml)
|
|
113
119
|
|
|
114
120
|
```bash
|
|
115
121
|
curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.yml
|
|
116
122
|
docker compose up -d
|
|
117
123
|
# Open http://localhost:8080
|
|
124
|
+
# If the dashboard is empty: docker compose logs seed
|
|
118
125
|
```
|
|
119
126
|
|
|
127
|
+
[MiniStack](https://github.com/ministackorg/ministack) derives the URLs it returns (SQS queue URLs, pre-signed URLs) from the request `Host`, so they resolve from other containers and from the host without extra configuration.
|
|
128
|
+
|
|
129
|
+
**Floci + StackPort** — [`examples/docker-compose.floci.yml`](examples/docker-compose.floci.yml)
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.floci.yml
|
|
133
|
+
docker compose -f docker-compose.floci.yml up -d
|
|
134
|
+
# Open http://localhost:8080
|
|
135
|
+
# If the dashboard is empty: docker compose -f docker-compose.floci.yml logs seed
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
[Floci](https://github.com/floci-io/floci) takes the hostname for the URLs it returns from `FLOCI_HOSTNAME`, which the example sets to `floci` so they resolve from other containers. From the host, reach the same resources through `http://localhost:4566`.
|
|
139
|
+
|
|
120
140
|
### Docker (standalone)
|
|
121
141
|
|
|
122
142
|
```bash
|
|
@@ -206,6 +226,21 @@ You can test connections before saving using the "Test Connection" button in the
|
|
|
206
226
|
|
|
207
227
|
All other services use a searchable resource table with JSON detail view, pagination, and export (JSON/CSV).
|
|
208
228
|
|
|
229
|
+
## Learn
|
|
230
|
+
|
|
231
|
+
Guided lessons that run inside StackPort itself, against your own emulator. A panel walks you through a lesson step by step while hotspots point at the actual controls, and StackPort checks the live resource state, so a step only turns green when the thing genuinely exists.
|
|
232
|
+
|
|
233
|
+
Open **Learn** in the sidebar (or press `g l`) and start a lesson. Every step gives you two ways through it:
|
|
234
|
+
|
|
235
|
+
- **Do it here** — the popover points at the control to use, with the exact values to type, copyable.
|
|
236
|
+
- **With the CLI** — the command, already filled in with your endpoint and your resource names, plus an explanation of what each flag does.
|
|
237
|
+
|
|
238
|
+
Either way, verification is the same: StackPort calls the real API and reports what it found. Do the work in a terminal and the step turns green on its own within a few seconds, no clicking required. Nothing is a gate, either: any step can be skipped and re-checked later.
|
|
239
|
+
|
|
240
|
+
You pick the names. A lesson generates something like `stackport-learn-k3xz` and every command and field on screen uses it, so the commands can be copied and run without editing. Change it under "Names used in this lesson" and everything follows.
|
|
241
|
+
|
|
242
|
+
Lessons only run against a local emulator, since they create and delete resources. Progress is stored locally in `~/.stackport/learn_progress.json`. Set `STACKPORT_LEARN=false` to remove Learn entirely.
|
|
243
|
+
|
|
209
244
|
## Tag Management
|
|
210
245
|
|
|
211
246
|
Unified tag read/write across 21 resource types:
|
|
@@ -244,6 +279,7 @@ Press `?` anywhere to see all shortcuts.
|
|
|
244
279
|
| `b` | Toggle sidebar |
|
|
245
280
|
| `g d` | Go to Dashboard |
|
|
246
281
|
| `g r` | Go to Resources |
|
|
282
|
+
| `g l` | Go to Learn |
|
|
247
283
|
| `/` | Focus search |
|
|
248
284
|
| `j` / `k` | Navigate up/down in resource list |
|
|
249
285
|
| `[` / `]` | Previous/next service |
|
|
@@ -263,12 +299,13 @@ Press `?` anywhere to see all shortcuts.
|
|
|
263
299
|
| `STACKPORT_PORT` | `8080` | StackPort server port |
|
|
264
300
|
| `STACKPORT_ALLOW_WRITES` | `true` | Enable write operations (POST/PUT/DELETE) |
|
|
265
301
|
| `STACKPORT_S3_MAX_UPLOAD_MB` | `100` | Max S3 upload size per object (MiB) |
|
|
302
|
+
| `STACKPORT_LEARN` | `true` | Guided tutorials. `false` removes the Learn routes and UI |
|
|
266
303
|
| `STACKPORT_SERVICES` | *(35 services)* | Comma-separated list of services to probe |
|
|
267
304
|
| `STACKPORT_PROBE_TIMEOUT` | `5` | Seconds before a service probe times out |
|
|
268
305
|
| `STACKPORT_CACHE_TTL` | `5` | Seconds to cache service stats |
|
|
269
306
|
| `STACKPORT_PROBE_WORKERS` | `10` | Max concurrent workers for service probing |
|
|
270
307
|
| `STACKPORT_ENDPOINTS` | *(unset)* | Multiple endpoints: `local=http://localhost:4566,staging=http://...` |
|
|
271
|
-
| `STACKPORT_DATA_DIR` | `~/.stackport` | Directory for persistent config (endpoints.json) |
|
|
308
|
+
| `STACKPORT_DATA_DIR` | `~/.stackport` | Directory for persistent config (endpoints.json, learn_progress.json) |
|
|
272
309
|
| `LOG_LEVEL` | `INFO` | Python log level (`DEBUG` shows healthcheck logs) |
|
|
273
310
|
|
|
274
311
|
## Supported Services (35)
|
|
@@ -333,6 +370,10 @@ ui/src/
|
|
|
333
370
|
lib/ API client, types, service icons, utils
|
|
334
371
|
```
|
|
335
372
|
|
|
373
|
+
## Origin
|
|
374
|
+
|
|
375
|
+
StackPort started as a web UI pull request to [MiniStack](https://github.com/ministackorg/ministack) ([ministack#116](https://github.com/ministackorg/ministack/pull/116)). The MiniStack maintainers suggested it would be more useful as a standalone tool that talks to any emulator over the standard AWS API, and that is what it became. Thanks to [@Nahuel990](https://github.com/Nahuel990) and [@McDoit](https://github.com/McDoit) for pushing it in that direction.
|
|
376
|
+
|
|
336
377
|
## Contributors
|
|
337
378
|
|
|
338
379
|
<a href="https://github.com/DaviReisVieira/stackport/graphs/contributors">
|
|
@@ -34,19 +34,23 @@
|
|
|
34
34
|
- **Write operations** — upload/delete S3 objects, query DynamoDB, invoke Lambda, send/receive SQS messages
|
|
35
35
|
- **Real AWS support** — connect to real AWS accounts with read-only mode by default
|
|
36
36
|
- **Per-endpoint authentication** — default credentials, AWS profiles (SSO/AssumeRole), or static keys per endpoint
|
|
37
|
+
- **Guided lessons** — hands-on AWS tutorials that run inside the console and verify your real resource state
|
|
37
38
|
- **Tag management** — unified tagging across 21 resource types
|
|
38
39
|
- **CLI** — `stackport status`, `list`, `describe`, `export` with JSON/CSV/table output
|
|
39
40
|
- **Real-time dashboard** with WebSocket-powered live updates
|
|
40
41
|
- **Keyboard shortcuts** — 16 shortcuts for fast navigation (press `?` to view)
|
|
41
|
-
- Single Docker image, works with MiniStack, LocalStack, Moto, or any AWS-compatible endpoint
|
|
42
|
+
- Single Docker image, works with [MiniStack](https://github.com/ministackorg/ministack), [Floci](https://github.com/floci-io/floci), LocalStack, Moto, or any AWS-compatible endpoint
|
|
42
43
|
|
|
43
44
|
## Quick Start
|
|
44
45
|
|
|
45
|
-
### With a local emulator
|
|
46
|
+
### With a local emulator
|
|
47
|
+
|
|
48
|
+
Start whichever emulator you use, then point StackPort at it:
|
|
46
49
|
|
|
47
50
|
```bash
|
|
48
|
-
# Start
|
|
49
|
-
|
|
51
|
+
# Start an emulator on :4566 (pick one)
|
|
52
|
+
docker run -d -p 4566:4566 ministackorg/ministack # MiniStack
|
|
53
|
+
docker run -d -p 4566:4566 floci/floci # Floci
|
|
50
54
|
|
|
51
55
|
# Start StackPort
|
|
52
56
|
pip install stackport
|
|
@@ -71,16 +75,32 @@ You can also configure per-endpoint authentication from the Settings UI — sele
|
|
|
71
75
|
|
|
72
76
|
When connected to real AWS, StackPort shows a warning banner and operates in read-only mode unless writes are explicitly enabled.
|
|
73
77
|
|
|
74
|
-
### Docker Compose
|
|
78
|
+
### Docker Compose
|
|
79
|
+
|
|
80
|
+
Each example runs the emulator, StackPort, and a one-shot `seed` container that creates a few resources (S3, SQS, SNS, DynamoDB, Secrets Manager, IAM, Logs, Step Functions) so the dashboard has something to show on first load. Both use the same seed. Swap in LocalStack, Moto, or any AWS-compatible endpoint by replacing the emulator service and updating `AWS_ENDPOINT_URL`.
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
**MiniStack + StackPort** — [`examples/docker-compose.yml`](examples/docker-compose.yml)
|
|
77
83
|
|
|
78
84
|
```bash
|
|
79
85
|
curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.yml
|
|
80
86
|
docker compose up -d
|
|
81
87
|
# Open http://localhost:8080
|
|
88
|
+
# If the dashboard is empty: docker compose logs seed
|
|
82
89
|
```
|
|
83
90
|
|
|
91
|
+
[MiniStack](https://github.com/ministackorg/ministack) derives the URLs it returns (SQS queue URLs, pre-signed URLs) from the request `Host`, so they resolve from other containers and from the host without extra configuration.
|
|
92
|
+
|
|
93
|
+
**Floci + StackPort** — [`examples/docker-compose.floci.yml`](examples/docker-compose.floci.yml)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.floci.yml
|
|
97
|
+
docker compose -f docker-compose.floci.yml up -d
|
|
98
|
+
# Open http://localhost:8080
|
|
99
|
+
# If the dashboard is empty: docker compose -f docker-compose.floci.yml logs seed
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
[Floci](https://github.com/floci-io/floci) takes the hostname for the URLs it returns from `FLOCI_HOSTNAME`, which the example sets to `floci` so they resolve from other containers. From the host, reach the same resources through `http://localhost:4566`.
|
|
103
|
+
|
|
84
104
|
### Docker (standalone)
|
|
85
105
|
|
|
86
106
|
```bash
|
|
@@ -170,6 +190,21 @@ You can test connections before saving using the "Test Connection" button in the
|
|
|
170
190
|
|
|
171
191
|
All other services use a searchable resource table with JSON detail view, pagination, and export (JSON/CSV).
|
|
172
192
|
|
|
193
|
+
## Learn
|
|
194
|
+
|
|
195
|
+
Guided lessons that run inside StackPort itself, against your own emulator. A panel walks you through a lesson step by step while hotspots point at the actual controls, and StackPort checks the live resource state, so a step only turns green when the thing genuinely exists.
|
|
196
|
+
|
|
197
|
+
Open **Learn** in the sidebar (or press `g l`) and start a lesson. Every step gives you two ways through it:
|
|
198
|
+
|
|
199
|
+
- **Do it here** — the popover points at the control to use, with the exact values to type, copyable.
|
|
200
|
+
- **With the CLI** — the command, already filled in with your endpoint and your resource names, plus an explanation of what each flag does.
|
|
201
|
+
|
|
202
|
+
Either way, verification is the same: StackPort calls the real API and reports what it found. Do the work in a terminal and the step turns green on its own within a few seconds, no clicking required. Nothing is a gate, either: any step can be skipped and re-checked later.
|
|
203
|
+
|
|
204
|
+
You pick the names. A lesson generates something like `stackport-learn-k3xz` and every command and field on screen uses it, so the commands can be copied and run without editing. Change it under "Names used in this lesson" and everything follows.
|
|
205
|
+
|
|
206
|
+
Lessons only run against a local emulator, since they create and delete resources. Progress is stored locally in `~/.stackport/learn_progress.json`. Set `STACKPORT_LEARN=false` to remove Learn entirely.
|
|
207
|
+
|
|
173
208
|
## Tag Management
|
|
174
209
|
|
|
175
210
|
Unified tag read/write across 21 resource types:
|
|
@@ -208,6 +243,7 @@ Press `?` anywhere to see all shortcuts.
|
|
|
208
243
|
| `b` | Toggle sidebar |
|
|
209
244
|
| `g d` | Go to Dashboard |
|
|
210
245
|
| `g r` | Go to Resources |
|
|
246
|
+
| `g l` | Go to Learn |
|
|
211
247
|
| `/` | Focus search |
|
|
212
248
|
| `j` / `k` | Navigate up/down in resource list |
|
|
213
249
|
| `[` / `]` | Previous/next service |
|
|
@@ -227,12 +263,13 @@ Press `?` anywhere to see all shortcuts.
|
|
|
227
263
|
| `STACKPORT_PORT` | `8080` | StackPort server port |
|
|
228
264
|
| `STACKPORT_ALLOW_WRITES` | `true` | Enable write operations (POST/PUT/DELETE) |
|
|
229
265
|
| `STACKPORT_S3_MAX_UPLOAD_MB` | `100` | Max S3 upload size per object (MiB) |
|
|
266
|
+
| `STACKPORT_LEARN` | `true` | Guided tutorials. `false` removes the Learn routes and UI |
|
|
230
267
|
| `STACKPORT_SERVICES` | *(35 services)* | Comma-separated list of services to probe |
|
|
231
268
|
| `STACKPORT_PROBE_TIMEOUT` | `5` | Seconds before a service probe times out |
|
|
232
269
|
| `STACKPORT_CACHE_TTL` | `5` | Seconds to cache service stats |
|
|
233
270
|
| `STACKPORT_PROBE_WORKERS` | `10` | Max concurrent workers for service probing |
|
|
234
271
|
| `STACKPORT_ENDPOINTS` | *(unset)* | Multiple endpoints: `local=http://localhost:4566,staging=http://...` |
|
|
235
|
-
| `STACKPORT_DATA_DIR` | `~/.stackport` | Directory for persistent config (endpoints.json) |
|
|
272
|
+
| `STACKPORT_DATA_DIR` | `~/.stackport` | Directory for persistent config (endpoints.json, learn_progress.json) |
|
|
236
273
|
| `LOG_LEVEL` | `INFO` | Python log level (`DEBUG` shows healthcheck logs) |
|
|
237
274
|
|
|
238
275
|
## Supported Services (35)
|
|
@@ -297,6 +334,10 @@ ui/src/
|
|
|
297
334
|
lib/ API client, types, service icons, utils
|
|
298
335
|
```
|
|
299
336
|
|
|
337
|
+
## Origin
|
|
338
|
+
|
|
339
|
+
StackPort started as a web UI pull request to [MiniStack](https://github.com/ministackorg/ministack) ([ministack#116](https://github.com/ministackorg/ministack/pull/116)). The MiniStack maintainers suggested it would be more useful as a standalone tool that talks to any emulator over the standard AWS API, and that is what it became. Thanks to [@Nahuel990](https://github.com/Nahuel990) and [@McDoit](https://github.com/McDoit) for pushing it in that direction.
|
|
340
|
+
|
|
300
341
|
## Contributors
|
|
301
342
|
|
|
302
343
|
<a href="https://github.com/DaviReisVieira/stackport/graphs/contributors">
|
|
@@ -10,13 +10,37 @@ import click
|
|
|
10
10
|
import uvicorn
|
|
11
11
|
|
|
12
12
|
from backend.aws_client import get_client
|
|
13
|
-
from backend.config import
|
|
14
|
-
|
|
13
|
+
from backend.config import (
|
|
14
|
+
AWS_ENDPOINT_URL,
|
|
15
|
+
AWS_REGION,
|
|
16
|
+
LOG_LEVEL,
|
|
17
|
+
STACKPORT_PORT,
|
|
18
|
+
STACKPORT_SERVICES,
|
|
19
|
+
endpoint_store,
|
|
20
|
+
)
|
|
21
|
+
from backend.routes.resources import (
|
|
22
|
+
DESCRIBE_REGISTRY,
|
|
23
|
+
_PREFERRED_ID_FIELD,
|
|
24
|
+
_extract_id,
|
|
25
|
+
_serialize,
|
|
26
|
+
)
|
|
15
27
|
from backend.routes.stats import SERVICE_REGISTRY, _METHOD_KWARGS, _count_items, _probe_service
|
|
16
28
|
|
|
17
29
|
logger = logging.getLogger(__name__)
|
|
18
30
|
|
|
19
31
|
|
|
32
|
+
def _client_kwargs(endpoint: str | None, region: str | None) -> dict:
|
|
33
|
+
"""Resolve where a CLI command talks to.
|
|
34
|
+
|
|
35
|
+
An explicit --endpoint (or AWS_ENDPOINT_URL at startup) wins. Without one,
|
|
36
|
+
fall back to the saved default endpoint, the same one the web UI uses.
|
|
37
|
+
Passing the result to get_client explicitly is what makes the flag work
|
|
38
|
+
once ~/.stackport/endpoints.json exists.
|
|
39
|
+
"""
|
|
40
|
+
url = endpoint if endpoint is not None else endpoint_store.get_default_url()
|
|
41
|
+
return {"endpoint_url": url, "region": region}
|
|
42
|
+
|
|
43
|
+
|
|
20
44
|
@click.group(invoke_without_command=True)
|
|
21
45
|
@click.version_option(package_name="stackport")
|
|
22
46
|
@click.pass_context
|
|
@@ -39,22 +63,14 @@ def serve(port):
|
|
|
39
63
|
@click.option("--output", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
40
64
|
def status(endpoint, region, output):
|
|
41
65
|
"""Show all services with availability and resource counts."""
|
|
42
|
-
|
|
43
|
-
if endpoint != AWS_ENDPOINT_URL:
|
|
44
|
-
import os
|
|
45
|
-
|
|
46
|
-
os.environ["AWS_ENDPOINT_URL"] = endpoint
|
|
47
|
-
if region != AWS_REGION:
|
|
48
|
-
import os
|
|
49
|
-
|
|
50
|
-
os.environ["AWS_REGION"] = region
|
|
66
|
+
client_kwargs = _client_kwargs(endpoint, region)
|
|
51
67
|
|
|
52
68
|
enabled_services = [s.strip() for s in STACKPORT_SERVICES.split(",") if s.strip()]
|
|
53
69
|
services = {}
|
|
54
70
|
|
|
55
71
|
try:
|
|
56
72
|
with ThreadPoolExecutor(max_workers=min(len(enabled_services), 10)) as executor:
|
|
57
|
-
futures = {executor.submit(_probe_service, svc): svc for svc in enabled_services}
|
|
73
|
+
futures = {executor.submit(_probe_service, svc, **client_kwargs): svc for svc in enabled_services}
|
|
58
74
|
for future in as_completed(futures):
|
|
59
75
|
svc_name, result = future.result()
|
|
60
76
|
services[svc_name] = result
|
|
@@ -89,15 +105,7 @@ def status(endpoint, region, output):
|
|
|
89
105
|
@click.option("--output", type=click.Choice(["json", "table", "csv"]), default="table", help="Output format")
|
|
90
106
|
def list(service, endpoint, region, output):
|
|
91
107
|
"""List resources for a service."""
|
|
92
|
-
|
|
93
|
-
if endpoint != AWS_ENDPOINT_URL:
|
|
94
|
-
import os
|
|
95
|
-
|
|
96
|
-
os.environ["AWS_ENDPOINT_URL"] = endpoint
|
|
97
|
-
if region != AWS_REGION:
|
|
98
|
-
import os
|
|
99
|
-
|
|
100
|
-
os.environ["AWS_REGION"] = region
|
|
108
|
+
client_kwargs = _client_kwargs(endpoint, region)
|
|
101
109
|
|
|
102
110
|
registry_entries = SERVICE_REGISTRY.get(service)
|
|
103
111
|
if not registry_entries:
|
|
@@ -109,7 +117,7 @@ def list(service, endpoint, region, output):
|
|
|
109
117
|
resources = {}
|
|
110
118
|
for resource_type, boto3_service, method_name, response_key in registry_entries:
|
|
111
119
|
try:
|
|
112
|
-
client = get_client(boto3_service)
|
|
120
|
+
client = get_client(boto3_service, **client_kwargs)
|
|
113
121
|
method = getattr(client, method_name)
|
|
114
122
|
kwargs = _METHOD_KWARGS.get((boto3_service, method_name), {})
|
|
115
123
|
resp = method(**kwargs)
|
|
@@ -129,8 +137,9 @@ def list(service, endpoint, region, output):
|
|
|
129
137
|
writer = csv.writer(sys.stdout)
|
|
130
138
|
writer.writerow(["service", "resource_type", "resource_id", "name"])
|
|
131
139
|
for resource_type, items in resources.items():
|
|
140
|
+
preferred = _PREFERRED_ID_FIELD.get((service, resource_type))
|
|
132
141
|
for item in items:
|
|
133
|
-
resource_id = _extract_id(item)
|
|
142
|
+
resource_id = _extract_id(item, preferred)
|
|
134
143
|
name = item.get("Name", item.get("FunctionName", item.get("TableName", resource_id))) if isinstance(item, dict) else resource_id
|
|
135
144
|
writer.writerow([service, resource_type, resource_id, name])
|
|
136
145
|
else:
|
|
@@ -139,8 +148,9 @@ def list(service, endpoint, region, output):
|
|
|
139
148
|
if items:
|
|
140
149
|
click.echo(f"\n{resource_type.upper()} ({len(items)}):")
|
|
141
150
|
click.echo("-" * 60)
|
|
151
|
+
preferred = _PREFERRED_ID_FIELD.get((service, resource_type))
|
|
142
152
|
for item in items[:20]: # Limit to first 20 for readability
|
|
143
|
-
resource_id = _extract_id(item)
|
|
153
|
+
resource_id = _extract_id(item, preferred)
|
|
144
154
|
if isinstance(item, dict):
|
|
145
155
|
name = item.get("Name", item.get("FunctionName", item.get("TableName", "")))
|
|
146
156
|
if name and name != resource_id:
|
|
@@ -164,20 +174,12 @@ def list(service, endpoint, region, output):
|
|
|
164
174
|
@click.option("--output", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
165
175
|
def describe(service, resource_type, resource_id, endpoint, region, output):
|
|
166
176
|
"""Describe a specific resource."""
|
|
167
|
-
|
|
168
|
-
if endpoint != AWS_ENDPOINT_URL:
|
|
169
|
-
import os
|
|
170
|
-
|
|
171
|
-
os.environ["AWS_ENDPOINT_URL"] = endpoint
|
|
172
|
-
if region != AWS_REGION:
|
|
173
|
-
import os
|
|
174
|
-
|
|
175
|
-
os.environ["AWS_REGION"] = region
|
|
177
|
+
client_kwargs = _client_kwargs(endpoint, region)
|
|
176
178
|
|
|
177
179
|
# Special case for WAFv2
|
|
178
180
|
if (service, resource_type) == ("wafv2", "web_acls"):
|
|
179
181
|
try:
|
|
180
|
-
client = get_client("wafv2")
|
|
182
|
+
client = get_client("wafv2", **client_kwargs)
|
|
181
183
|
acls = client.list_web_acls(Scope="REGIONAL").get("WebACLs", [])
|
|
182
184
|
match = next((a for a in acls if a.get("Name") == resource_id), None)
|
|
183
185
|
if not match:
|
|
@@ -211,7 +213,7 @@ def describe(service, resource_type, resource_id, endpoint, region, output):
|
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
try:
|
|
214
|
-
client = get_client(boto3_service)
|
|
216
|
+
client = get_client(boto3_service, **client_kwargs)
|
|
215
217
|
method = getattr(client, method_name)
|
|
216
218
|
if id_param in _LIST_PARAMS:
|
|
217
219
|
resp = method(**{id_param: [resource_id]})
|
|
@@ -16,6 +16,9 @@ AWS_ACCESS_KEY_ID: str | None = os.environ.get("AWS_ACCESS_KEY_ID")
|
|
|
16
16
|
AWS_SECRET_ACCESS_KEY: str | None = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
|
17
17
|
STACKPORT_ALLOW_WRITES: bool = os.environ.get("STACKPORT_ALLOW_WRITES", "true").lower() in ("1", "true", "yes")
|
|
18
18
|
STACKPORT_PORT: int = int(os.environ.get("STACKPORT_PORT", "8080"))
|
|
19
|
+
# Guided tutorials. Set to false to drop the Learn routes and every trace of
|
|
20
|
+
# the tutorial UI, for anyone who wants StackPort as a resource browser only.
|
|
21
|
+
STACKPORT_LEARN: bool = os.environ.get("STACKPORT_LEARN", "true").lower() in ("1", "true", "yes")
|
|
19
22
|
STACKPORT_SERVICES: str = os.environ.get(
|
|
20
23
|
"STACKPORT_SERVICES",
|
|
21
24
|
"s3,sqs,sns,dynamodb,lambda,iam,logs,ssm,secretsmanager,kinesis,events,ec2,"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Learn module: guided AWS trails verified against live emulator state."""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Validation for authored trail content.
|
|
2
|
+
|
|
3
|
+
A typo in a trail JSON file, a verification type that does not exist or a step
|
|
4
|
+
pointing at a hotspot the UI never renders would all strand a learner mid
|
|
5
|
+
lesson with no way forward. Content is therefore validated when it loads, and
|
|
6
|
+
the test suite runs the same validation over everything that ships, so bad
|
|
7
|
+
content fails in CI instead of in front of someone trying to learn.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from backend.learn.hotspots import KNOWN_HOTSPOT_IDS
|
|
13
|
+
from backend.learn.verify import CHECKS
|
|
14
|
+
|
|
15
|
+
_PLACEHOLDER_RE = re.compile(r"\{\{(\w+)\}\}")
|
|
16
|
+
|
|
17
|
+
# Filled in by the server rather than declared by a lesson
|
|
18
|
+
_IMPLICIT_VARIABLES = frozenset({"endpointUrl"})
|
|
19
|
+
|
|
20
|
+
_TRAIL_KEYS = ("id", "title", "description", "lessons")
|
|
21
|
+
_LESSON_KEYS = ("id", "title", "summary", "steps")
|
|
22
|
+
_STEP_KEYS = ("id", "title", "instruction")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _require(data: dict, keys: tuple[str, ...], where: str) -> None:
|
|
26
|
+
missing = [key for key in keys if key not in data]
|
|
27
|
+
if missing:
|
|
28
|
+
raise ValueError(f"{where} is missing {', '.join(missing)}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _validate_verify(spec: dict, where: str) -> None:
|
|
32
|
+
check_type = spec.get("type")
|
|
33
|
+
if check_type not in CHECKS:
|
|
34
|
+
raise ValueError(f"{where} uses unknown verify type '{check_type}'")
|
|
35
|
+
if check_type == "all_of":
|
|
36
|
+
for nested in spec.get("params", {}).get("checks", []):
|
|
37
|
+
_validate_verify(nested, f"{where} (nested)")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_trail(trail: dict) -> None:
|
|
41
|
+
"""Raise ValueError describing the first problem found, if any."""
|
|
42
|
+
_require(trail, _TRAIL_KEYS, "trail")
|
|
43
|
+
if not trail["lessons"]:
|
|
44
|
+
raise ValueError(f"trail '{trail['id']}' has no lessons")
|
|
45
|
+
|
|
46
|
+
lesson_ids: set[str] = set()
|
|
47
|
+
for lesson in trail["lessons"]:
|
|
48
|
+
_require(lesson, _LESSON_KEYS, f"lesson in trail '{trail['id']}'")
|
|
49
|
+
where_lesson = f"{trail['id']}/{lesson['id']}"
|
|
50
|
+
if lesson["id"] in lesson_ids:
|
|
51
|
+
raise ValueError(f"duplicate lesson id '{lesson['id']}' in trail '{trail['id']}'")
|
|
52
|
+
lesson_ids.add(lesson["id"])
|
|
53
|
+
|
|
54
|
+
declared = {spec["name"] for spec in lesson.get("variables", [])} | _IMPLICIT_VARIABLES
|
|
55
|
+
for spec in lesson.get("variables", []):
|
|
56
|
+
if "default" not in spec:
|
|
57
|
+
raise ValueError(f"variable '{spec['name']}' in {where_lesson} has no default")
|
|
58
|
+
|
|
59
|
+
if not lesson["steps"]:
|
|
60
|
+
raise ValueError(f"lesson {where_lesson} has no steps")
|
|
61
|
+
|
|
62
|
+
# everything except the variable specs, whose defaults carry {{suffix}}
|
|
63
|
+
body = {key: value for key, value in lesson.items() if key != "variables"}
|
|
64
|
+
for name in _placeholders(body) - declared:
|
|
65
|
+
raise ValueError(f"lesson {where_lesson} uses undeclared variable '{{{{{name}}}}}'")
|
|
66
|
+
|
|
67
|
+
step_ids: set[str] = set()
|
|
68
|
+
hotspot_ids: set[str] = set()
|
|
69
|
+
for step in lesson["steps"]:
|
|
70
|
+
_require(step, _STEP_KEYS, f"step in lesson {where_lesson}")
|
|
71
|
+
where = f"{where_lesson}/{step['id']}"
|
|
72
|
+
if step["id"] in step_ids:
|
|
73
|
+
raise ValueError(f"duplicate step id '{step['id']}' in lesson {where_lesson}")
|
|
74
|
+
step_ids.add(step["id"])
|
|
75
|
+
|
|
76
|
+
if step.get("verify"):
|
|
77
|
+
_validate_verify(step["verify"], where)
|
|
78
|
+
|
|
79
|
+
console = step.get("console")
|
|
80
|
+
if console:
|
|
81
|
+
hotspot_id = console.get("hotspotId")
|
|
82
|
+
if hotspot_id and hotspot_id not in KNOWN_HOTSPOT_IDS:
|
|
83
|
+
raise ValueError(f"{where} points at unknown hotspot '{hotspot_id}'")
|
|
84
|
+
if hotspot_id in hotspot_ids:
|
|
85
|
+
# two steps sharing an anchor would unlock the second one's
|
|
86
|
+
# Next button as soon as the first one mounted
|
|
87
|
+
raise ValueError(f"{where} reuses hotspot '{hotspot_id}' within the same lesson")
|
|
88
|
+
if hotspot_id:
|
|
89
|
+
hotspot_ids.add(hotspot_id)
|
|
90
|
+
if not console.get("route"):
|
|
91
|
+
raise ValueError(f"{where} has a console path with no route")
|
|
92
|
+
|
|
93
|
+
for name in _placeholders(step) - declared:
|
|
94
|
+
raise ValueError(f"{where} uses undeclared variable '{{{{{name}}}}}'")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _placeholders(value: object) -> set[str]:
|
|
98
|
+
found: set[str] = set()
|
|
99
|
+
if isinstance(value, str):
|
|
100
|
+
found.update(_PLACEHOLDER_RE.findall(value))
|
|
101
|
+
elif isinstance(value, dict):
|
|
102
|
+
for nested in value.values():
|
|
103
|
+
found |= _placeholders(nested)
|
|
104
|
+
elif isinstance(value, list):
|
|
105
|
+
for nested in value:
|
|
106
|
+
found |= _placeholders(nested)
|
|
107
|
+
return found
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Hotspot ids a lesson step may point at.
|
|
2
|
+
|
|
3
|
+
A hotspot is an anchor rendered by the frontend next to a real control, which
|
|
4
|
+
the tutorial popover attaches to. The ids live here because trail content is
|
|
5
|
+
validated on the backend, and are mirrored in
|
|
6
|
+
ui/src/components/cloudscape/learn/hotspots.ts, where the anchors are actually
|
|
7
|
+
rendered. A frontend test reads this file and fails if the two lists drift, so
|
|
8
|
+
keep it plain data: one string literal per line, no logic.
|
|
9
|
+
|
|
10
|
+
Naming: {scope}-{noun}-{action}.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
KNOWN_HOTSPOT_IDS: frozenset[str] = frozenset(
|
|
14
|
+
{
|
|
15
|
+
# shell
|
|
16
|
+
"learn-panel-anchor",
|
|
17
|
+
"nav-resources",
|
|
18
|
+
# s3
|
|
19
|
+
"s3-buckets-create",
|
|
20
|
+
"s3-buckets-table",
|
|
21
|
+
"s3-upload",
|
|
22
|
+
"s3-objects-table",
|
|
23
|
+
# dynamodb
|
|
24
|
+
"dynamodb-tables-create",
|
|
25
|
+
"dynamodb-tables-table",
|
|
26
|
+
}
|
|
27
|
+
)
|