stackport 0.4.2__tar.gz → 0.4.4__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.2/stackport.egg-info → stackport-0.4.4}/PKG-INFO +31 -8
- {stackport-0.4.2 → stackport-0.4.4}/README.md +29 -6
- {stackport-0.4.2 → stackport-0.4.4}/backend/cli.py +37 -35
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/dynamodb.py +66 -11
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/resources.py +6 -0
- {stackport-0.4.2 → stackport-0.4.4}/pyproject.toml +2 -2
- {stackport-0.4.2 → stackport-0.4.4/stackport.egg-info}/PKG-INFO +31 -8
- {stackport-0.4.2 → stackport-0.4.4}/stackport.egg-info/SOURCES.txt +26 -26
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_cli.py +65 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_dynamodb_routes.py +167 -0
- stackport-0.4.4/tests/test_registries.py +142 -0
- stackport-0.4.2/ui/dist/assets/CloudscapeAbout-DccVyQFu.js → stackport-0.4.4/ui/dist/assets/CloudscapeAbout-BRbsDskR.js +1 -1
- stackport-0.4.2/ui/dist/assets/CloudscapeDashboard-DtV4-eQq.js → stackport-0.4.4/ui/dist/assets/CloudscapeDashboard-j2f4Elz2.js +1 -1
- stackport-0.4.2/ui/dist/assets/CloudscapeLearn-DW6RZAj4.js → stackport-0.4.4/ui/dist/assets/CloudscapeLearn-CVW3k3X0.js +1 -1
- stackport-0.4.2/ui/dist/assets/CloudscapeResourceBrowser-FpsXtjf_.js → stackport-0.4.4/ui/dist/assets/CloudscapeResourceBrowser-CgWdgn20.js +10 -10
- stackport-0.4.2/ui/dist/assets/CloudscapeSettings-UdgiI6dm.js → stackport-0.4.4/ui/dist/assets/CloudscapeSettings-CIXFvPKv.js +1 -1
- stackport-0.4.2/ui/dist/assets/CloudscapeShell-Be6uFzsz.js → stackport-0.4.4/ui/dist/assets/CloudscapeShell-DNfhSg0O.js +1 -1
- stackport-0.4.2/ui/dist/assets/StateMachineGraph-G76o42iO.js → stackport-0.4.4/ui/dist/assets/StateMachineGraph-DvF_kIOO.js +1 -1
- stackport-0.4.2/ui/dist/assets/index-B3zGVRXf.js → stackport-0.4.4/ui/dist/assets/index-BC7E-Fm4.js +1 -1
- stackport-0.4.2/ui/dist/assets/index-hhBdqMqs.js → stackport-0.4.4/ui/dist/assets/index-BLdRlH9d.js +2 -2
- stackport-0.4.2/ui/dist/assets/index-B3bka_BF.js → stackport-0.4.4/ui/dist/assets/index-Ba-oOcd3.js +1 -1
- stackport-0.4.2/ui/dist/assets/index-DhD1oh4N.js → stackport-0.4.4/ui/dist/assets/index-BkSgVQ0X.js +1 -1
- stackport-0.4.2/ui/dist/assets/index-CNWD72L_.js → stackport-0.4.4/ui/dist/assets/index-C5z3cir-.js +1 -1
- stackport-0.4.2/ui/dist/assets/index-B33aGwWE.js → stackport-0.4.4/ui/dist/assets/index-Dw6TGweb.js +1 -1
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/index.html +1 -1
- stackport-0.4.2/tests/test_registries.py +0 -69
- {stackport-0.4.2 → stackport-0.4.4}/LICENSE +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/MANIFEST.in +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/__init__.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/aws_client.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/cache.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/config.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/endpoint_store.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/learn/__init__.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/learn/content.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/learn/hotspots.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/learn/trails/aws-first-steps.json +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/learn/verify.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/learn_store.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/main.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/__init__.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/common.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/ec2.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/endpoints.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/iam.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/lambda_svc.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/learn.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/logs.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/monitoring.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/rds.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/s3.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/secretsmanager.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/sns.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/sqs.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/stats.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/stepfunctions.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/routes/tags.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/__init__.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/dynamodb.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/endpoints.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/lambda_svc.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/learn.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/logs.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/monitoring.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/s3.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/secretsmanager.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/sns.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/sqs.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/stepfunctions.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/schemas/tags.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/backend/websocket.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/setup.cfg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/stackport.egg-info/requires.txt +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/stackport.egg-info/top_level.txt +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_cache.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_client.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_config.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_create_resources.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_ec2_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_endpoint_store.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_endpoints.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_iam_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_lambda_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_learn_content.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_learn_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_learn_store.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_logs_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_logs_tail.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_monitoring_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_multi_endpoint.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_rds_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_readonly_middleware.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_s3_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_s3_upload_limit_env.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_secretsmanager_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_sns_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_sqs_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_tags_routes.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/tests/test_websocket.py +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/assets/CloudscapeResourceBrowser-CzwePG-V.css +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/assets/CloudscapeShell-BXHVWjDE.css +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/assets/index-DE--A2EE.css +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/assets/index-DKIdXFX0.css +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/assets/index-DNBdeKId.css +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/assets/index-DoY0HscW.css +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/assets/index-DrkkXAoG.css +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/assets/index-Vf0UFiCT.css +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/assets/utils-Cfmwk2UT.js +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/acm.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/apigateway.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/appsync.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/athena.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/cloudformation.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/cloudfront.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/cognito-idp.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/dynamodb.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/ec2.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/ecr.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/ecs.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/elasticache.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/events.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/firehose.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/glue.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/iam.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/kinesis.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/kms.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/lambda.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/logs.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/monitoring.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/rds.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/route53.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/s3.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/secretsmanager.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/ses.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/sns.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/sqs.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/ssm.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/stepfunctions.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/aws-icons/wafv2.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/favicon.png +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/ui/dist/favicon.svg +0 -0
- {stackport-0.4.2 → stackport-0.4.4}/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.4
|
|
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
|
|
@@ -75,15 +75,18 @@ Dynamic: license-file
|
|
|
75
75
|
- **CLI** — `stackport status`, `list`, `describe`, `export` with JSON/CSV/table output
|
|
76
76
|
- **Real-time dashboard** with WebSocket-powered live updates
|
|
77
77
|
- **Keyboard shortcuts** — 16 shortcuts for fast navigation (press `?` to view)
|
|
78
|
-
- 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
|
|
79
79
|
|
|
80
80
|
## Quick Start
|
|
81
81
|
|
|
82
|
-
### With a local emulator
|
|
82
|
+
### With a local emulator
|
|
83
|
+
|
|
84
|
+
Start whichever emulator you use, then point StackPort at it:
|
|
83
85
|
|
|
84
86
|
```bash
|
|
85
|
-
# Start
|
|
86
|
-
|
|
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
|
|
87
90
|
|
|
88
91
|
# Start StackPort
|
|
89
92
|
pip install stackport
|
|
@@ -108,16 +111,32 @@ You can also configure per-endpoint authentication from the Settings UI — sele
|
|
|
108
111
|
|
|
109
112
|
When connected to real AWS, StackPort shows a warning banner and operates in read-only mode unless writes are explicitly enabled.
|
|
110
113
|
|
|
111
|
-
### 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`.
|
|
112
117
|
|
|
113
|
-
|
|
118
|
+
**MiniStack + StackPort** — [`examples/docker-compose.yml`](examples/docker-compose.yml)
|
|
114
119
|
|
|
115
120
|
```bash
|
|
116
121
|
curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.yml
|
|
117
122
|
docker compose up -d
|
|
118
123
|
# Open http://localhost:8080
|
|
124
|
+
# If the dashboard is empty: docker compose logs seed
|
|
119
125
|
```
|
|
120
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
|
+
|
|
121
140
|
### Docker (standalone)
|
|
122
141
|
|
|
123
142
|
```bash
|
|
@@ -351,6 +370,10 @@ ui/src/
|
|
|
351
370
|
lib/ API client, types, service icons, utils
|
|
352
371
|
```
|
|
353
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
|
+
|
|
354
377
|
## Contributors
|
|
355
378
|
|
|
356
379
|
<a href="https://github.com/DaviReisVieira/stackport/graphs/contributors">
|
|
@@ -39,15 +39,18 @@
|
|
|
39
39
|
- **CLI** — `stackport status`, `list`, `describe`, `export` with JSON/CSV/table output
|
|
40
40
|
- **Real-time dashboard** with WebSocket-powered live updates
|
|
41
41
|
- **Keyboard shortcuts** — 16 shortcuts for fast navigation (press `?` to view)
|
|
42
|
-
- 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
|
|
43
43
|
|
|
44
44
|
## Quick Start
|
|
45
45
|
|
|
46
|
-
### With a local emulator
|
|
46
|
+
### With a local emulator
|
|
47
|
+
|
|
48
|
+
Start whichever emulator you use, then point StackPort at it:
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
|
-
# Start
|
|
50
|
-
|
|
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
|
|
51
54
|
|
|
52
55
|
# Start StackPort
|
|
53
56
|
pip install stackport
|
|
@@ -72,16 +75,32 @@ You can also configure per-endpoint authentication from the Settings UI — sele
|
|
|
72
75
|
|
|
73
76
|
When connected to real AWS, StackPort shows a warning banner and operates in read-only mode unless writes are explicitly enabled.
|
|
74
77
|
|
|
75
|
-
### 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`.
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
**MiniStack + StackPort** — [`examples/docker-compose.yml`](examples/docker-compose.yml)
|
|
78
83
|
|
|
79
84
|
```bash
|
|
80
85
|
curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.yml
|
|
81
86
|
docker compose up -d
|
|
82
87
|
# Open http://localhost:8080
|
|
88
|
+
# If the dashboard is empty: docker compose logs seed
|
|
83
89
|
```
|
|
84
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
|
+
|
|
85
104
|
### Docker (standalone)
|
|
86
105
|
|
|
87
106
|
```bash
|
|
@@ -315,6 +334,10 @@ ui/src/
|
|
|
315
334
|
lib/ API client, types, service icons, utils
|
|
316
335
|
```
|
|
317
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
|
+
|
|
318
341
|
## Contributors
|
|
319
342
|
|
|
320
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]})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import base64
|
|
2
|
+
import binascii
|
|
2
3
|
import json
|
|
3
4
|
import logging
|
|
4
5
|
from typing import Any
|
|
@@ -27,6 +28,58 @@ def _plain_to_dynamodb_item(plain: dict[str, Any]) -> dict[str, Any]:
|
|
|
27
28
|
return {k: _serializer.serialize(v) for k, v in plain.items()}
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
# Binary (B / BS) travels as base64 over StackPort's HTTP API, the same way AWS
|
|
32
|
+
# writes it in DynamoDB JSON. boto3 hands us raw bytes, which JSON cannot carry.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _to_json_safe(obj: Any) -> Any:
|
|
36
|
+
"""Replace every bytes value with its base64 text. Safe to apply to whole
|
|
37
|
+
low-level responses: DynamoDB only returns bytes inside B and BS."""
|
|
38
|
+
if isinstance(obj, (bytes, bytearray)):
|
|
39
|
+
return base64.b64encode(obj).decode("ascii")
|
|
40
|
+
if isinstance(obj, dict):
|
|
41
|
+
return {k: _to_json_safe(v) for k, v in obj.items()}
|
|
42
|
+
if isinstance(obj, list):
|
|
43
|
+
return [_to_json_safe(v) for v in obj]
|
|
44
|
+
return obj
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _b64_to_bytes(value: Any, name: str) -> bytes:
|
|
48
|
+
if isinstance(value, (bytes, bytearray)):
|
|
49
|
+
return bytes(value)
|
|
50
|
+
try:
|
|
51
|
+
return base64.b64decode(value, validate=True)
|
|
52
|
+
except (binascii.Error, TypeError, ValueError) as e:
|
|
53
|
+
raise HTTPException(status_code=400, detail=f"Attribute {name!r} is not valid base64 for type B") from e
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _attr_from_json(av: Any, name: str) -> Any:
|
|
57
|
+
"""Inverse of _to_json_safe for one DynamoDB attribute value. Type-aware, so
|
|
58
|
+
strings under S stay strings and only B / BS are decoded."""
|
|
59
|
+
if not isinstance(av, dict):
|
|
60
|
+
return av
|
|
61
|
+
if "B" in av:
|
|
62
|
+
return {**av, "B": _b64_to_bytes(av["B"], name)}
|
|
63
|
+
if "BS" in av and isinstance(av["BS"], list):
|
|
64
|
+
return {**av, "BS": [_b64_to_bytes(v, name) for v in av["BS"]]}
|
|
65
|
+
if "L" in av and isinstance(av["L"], list):
|
|
66
|
+
return {**av, "L": [_attr_from_json(v, name) for v in av["L"]]}
|
|
67
|
+
if "M" in av and isinstance(av["M"], dict):
|
|
68
|
+
return {**av, "M": {k: _attr_from_json(v, f"{name}.{k}") for k, v in av["M"].items()}}
|
|
69
|
+
return av
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _item_from_json(item: dict[str, Any]) -> dict[str, Any]:
|
|
73
|
+
return {k: _attr_from_json(v, k) for k, v in item.items()}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _key_value(attr_type: str, value: str, name: str) -> dict[str, Any]:
|
|
77
|
+
"""Typed key value for a query condition. Binary keys arrive as base64."""
|
|
78
|
+
if attr_type == "B":
|
|
79
|
+
return {"B": _b64_to_bytes(value, name)}
|
|
80
|
+
return {attr_type: value}
|
|
81
|
+
|
|
82
|
+
|
|
30
83
|
def _get_partition_sort_keys(dynamodb: Any, table_name: str) -> tuple[str | None, str | None]:
|
|
31
84
|
table_resp = dynamodb.describe_table(TableName=table_name)
|
|
32
85
|
key_schema = table_resp["Table"].get("KeySchema", [])
|
|
@@ -56,7 +109,7 @@ def _coerce_item_dict(raw: dict[str, Any], item_format: str) -> dict[str, Any]:
|
|
|
56
109
|
return _plain_to_dynamodb_item(raw)
|
|
57
110
|
except TypeError as e:
|
|
58
111
|
raise HTTPException(status_code=400, detail=f"Could not convert plain item to DynamoDB types: {e}") from e
|
|
59
|
-
return raw
|
|
112
|
+
return _item_from_json(raw)
|
|
60
113
|
|
|
61
114
|
|
|
62
115
|
def _coerce_key_dict(raw: dict[str, Any], item_format: str) -> dict[str, Any]:
|
|
@@ -65,7 +118,7 @@ def _coerce_key_dict(raw: dict[str, Any], item_format: str) -> dict[str, Any]:
|
|
|
65
118
|
return _plain_to_dynamodb_item(raw)
|
|
66
119
|
except TypeError as e:
|
|
67
120
|
raise HTTPException(status_code=400, detail=f"Could not convert plain key to DynamoDB types: {e}") from e
|
|
68
|
-
return raw
|
|
121
|
+
return _item_from_json(raw)
|
|
69
122
|
|
|
70
123
|
|
|
71
124
|
def _invalidate_table_item_count(table_name: str, endpoint_url: str | None) -> None:
|
|
@@ -237,7 +290,7 @@ def scan_table(
|
|
|
237
290
|
if exclusive_start_key:
|
|
238
291
|
try:
|
|
239
292
|
decoded = base64.b64decode(exclusive_start_key).decode("utf-8")
|
|
240
|
-
scan_params["ExclusiveStartKey"] = json.loads(decoded)
|
|
293
|
+
scan_params["ExclusiveStartKey"] = _item_from_json(json.loads(decoded))
|
|
241
294
|
except Exception:
|
|
242
295
|
logger.debug("Invalid exclusive_start_key", exc_info=True)
|
|
243
296
|
|
|
@@ -248,11 +301,11 @@ def scan_table(
|
|
|
248
301
|
last_evaluated_key = resp.get("LastEvaluatedKey")
|
|
249
302
|
next_token = None
|
|
250
303
|
if last_evaluated_key:
|
|
251
|
-
next_token = base64.b64encode(json.dumps(last_evaluated_key).encode("utf-8")).decode("utf-8")
|
|
304
|
+
next_token = base64.b64encode(json.dumps(_to_json_safe(last_evaluated_key)).encode("utf-8")).decode("utf-8")
|
|
252
305
|
|
|
253
306
|
return {
|
|
254
307
|
"table": name,
|
|
255
|
-
"items": items,
|
|
308
|
+
"items": _to_json_safe(items),
|
|
256
309
|
"count": len(items),
|
|
257
310
|
"scanned_count": resp.get("ScannedCount", len(items)),
|
|
258
311
|
"next_token": next_token,
|
|
@@ -285,19 +338,19 @@ def query_table(name: str, request: QueryRequest, ep: EndpointInfo = Depends(get
|
|
|
285
338
|
# Build key condition expression
|
|
286
339
|
key_condition = f"#{partition_key} = :pk"
|
|
287
340
|
expression_attr_names = {f"#{partition_key}": partition_key}
|
|
288
|
-
expression_attr_values = {":pk":
|
|
341
|
+
expression_attr_values = {":pk": _key_value(partition_key_type, request.partition_key_value, partition_key)}
|
|
289
342
|
|
|
290
343
|
if sort_key and request.sort_key_value:
|
|
291
344
|
sort_key_type = attribute_defs.get(sort_key, "S")
|
|
292
345
|
if request.sort_key_operator == "=":
|
|
293
346
|
key_condition += f" AND #{sort_key} = :sk"
|
|
294
|
-
expression_attr_values[":sk"] =
|
|
347
|
+
expression_attr_values[":sk"] = _key_value(sort_key_type, request.sort_key_value, sort_key)
|
|
295
348
|
elif request.sort_key_operator in ("<", "<=", ">", ">="):
|
|
296
349
|
key_condition += f" AND #{sort_key} {request.sort_key_operator} :sk"
|
|
297
|
-
expression_attr_values[":sk"] =
|
|
350
|
+
expression_attr_values[":sk"] = _key_value(sort_key_type, request.sort_key_value, sort_key)
|
|
298
351
|
elif request.sort_key_operator == "BEGINS_WITH":
|
|
299
352
|
key_condition += f" AND begins_with(#{sort_key}, :sk)"
|
|
300
|
-
expression_attr_values[":sk"] =
|
|
353
|
+
expression_attr_values[":sk"] = _key_value(sort_key_type, request.sort_key_value, sort_key)
|
|
301
354
|
expression_attr_names[f"#{sort_key}"] = sort_key
|
|
302
355
|
|
|
303
356
|
query_params = {
|
|
@@ -313,10 +366,12 @@ def query_table(name: str, request: QueryRequest, ep: EndpointInfo = Depends(get
|
|
|
313
366
|
|
|
314
367
|
return {
|
|
315
368
|
"table": name,
|
|
316
|
-
"items": items,
|
|
369
|
+
"items": _to_json_safe(items),
|
|
317
370
|
"count": len(items),
|
|
318
371
|
"scanned_count": resp.get("ScannedCount", len(items)),
|
|
319
372
|
}
|
|
373
|
+
except HTTPException:
|
|
374
|
+
raise
|
|
320
375
|
except Exception as e:
|
|
321
376
|
logger.error("Failed to query table %s: %s", name, e, exc_info=True)
|
|
322
377
|
return {"error": str(e), "items": [], "count": 0}
|
|
@@ -411,7 +466,7 @@ def batch_write_items(
|
|
|
411
466
|
return {
|
|
412
467
|
"ok": True,
|
|
413
468
|
"table": name,
|
|
414
|
-
"unprocessed": uproc,
|
|
469
|
+
"unprocessed": _to_json_safe(uproc),
|
|
415
470
|
"message": "Some items were not processed; retry with returned keys.",
|
|
416
471
|
}
|
|
417
472
|
|
|
@@ -84,6 +84,12 @@ _PREFERRED_ID_FIELD: dict[tuple[str, str], str] = {
|
|
|
84
84
|
("elasticmapreduce", "clusters"): "Id",
|
|
85
85
|
("cognito-idp", "user_pools"): "Id",
|
|
86
86
|
("apigateway", "rest_apis"): "id",
|
|
87
|
+
("apigateway", "apis"): "ApiId",
|
|
88
|
+
("iam", "policies"): "Arn",
|
|
89
|
+
("ec2", "subnets"): "SubnetId",
|
|
90
|
+
("ec2", "security_groups"): "GroupId",
|
|
91
|
+
("elasticfilesystem", "file_systems"): "FileSystemId",
|
|
92
|
+
("rds", "db_clusters"): "DBClusterIdentifier",
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
# Known ID field names for extracting a resource identifier from list results
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stackport"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.4"
|
|
4
4
|
description = "Universal AWS resource browser for local emulators"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT"}
|
|
7
7
|
requires-python = ">=3.10"
|
|
8
8
|
authors = [{name = "Davi Reis Vieira"}]
|
|
9
|
-
keywords = ["aws", "ministack", "localstack", "browser", "devtools", "emulator"]
|
|
9
|
+
keywords = ["aws", "ministack", "floci", "localstack", "browser", "devtools", "emulator"]
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Development Status :: 4 - Beta",
|
|
12
12
|
"Framework :: FastAPI",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackport
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
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
|
|
@@ -75,15 +75,18 @@ Dynamic: license-file
|
|
|
75
75
|
- **CLI** — `stackport status`, `list`, `describe`, `export` with JSON/CSV/table output
|
|
76
76
|
- **Real-time dashboard** with WebSocket-powered live updates
|
|
77
77
|
- **Keyboard shortcuts** — 16 shortcuts for fast navigation (press `?` to view)
|
|
78
|
-
- 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
|
|
79
79
|
|
|
80
80
|
## Quick Start
|
|
81
81
|
|
|
82
|
-
### With a local emulator
|
|
82
|
+
### With a local emulator
|
|
83
|
+
|
|
84
|
+
Start whichever emulator you use, then point StackPort at it:
|
|
83
85
|
|
|
84
86
|
```bash
|
|
85
|
-
# Start
|
|
86
|
-
|
|
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
|
|
87
90
|
|
|
88
91
|
# Start StackPort
|
|
89
92
|
pip install stackport
|
|
@@ -108,16 +111,32 @@ You can also configure per-endpoint authentication from the Settings UI — sele
|
|
|
108
111
|
|
|
109
112
|
When connected to real AWS, StackPort shows a warning banner and operates in read-only mode unless writes are explicitly enabled.
|
|
110
113
|
|
|
111
|
-
### 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`.
|
|
112
117
|
|
|
113
|
-
|
|
118
|
+
**MiniStack + StackPort** — [`examples/docker-compose.yml`](examples/docker-compose.yml)
|
|
114
119
|
|
|
115
120
|
```bash
|
|
116
121
|
curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.yml
|
|
117
122
|
docker compose up -d
|
|
118
123
|
# Open http://localhost:8080
|
|
124
|
+
# If the dashboard is empty: docker compose logs seed
|
|
119
125
|
```
|
|
120
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
|
+
|
|
121
140
|
### Docker (standalone)
|
|
122
141
|
|
|
123
142
|
```bash
|
|
@@ -351,6 +370,10 @@ ui/src/
|
|
|
351
370
|
lib/ API client, types, service icons, utils
|
|
352
371
|
```
|
|
353
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
|
+
|
|
354
377
|
## Contributors
|
|
355
378
|
|
|
356
379
|
<a href="https://github.com/DaviReisVieira/stackport/graphs/contributors">
|
|
@@ -15,27 +15,27 @@ backend/../ui/dist/favicon.png
|
|
|
15
15
|
backend/../ui/dist/favicon.svg
|
|
16
16
|
backend/../ui/dist/index.html
|
|
17
17
|
backend/../ui/dist/stackport-logo.svg
|
|
18
|
-
backend/../ui/dist/assets/CloudscapeAbout-
|
|
19
|
-
backend/../ui/dist/assets/CloudscapeDashboard-
|
|
20
|
-
backend/../ui/dist/assets/CloudscapeLearn-
|
|
18
|
+
backend/../ui/dist/assets/CloudscapeAbout-BRbsDskR.js
|
|
19
|
+
backend/../ui/dist/assets/CloudscapeDashboard-j2f4Elz2.js
|
|
20
|
+
backend/../ui/dist/assets/CloudscapeLearn-CVW3k3X0.js
|
|
21
|
+
backend/../ui/dist/assets/CloudscapeResourceBrowser-CgWdgn20.js
|
|
21
22
|
backend/../ui/dist/assets/CloudscapeResourceBrowser-CzwePG-V.css
|
|
22
|
-
backend/../ui/dist/assets/
|
|
23
|
-
backend/../ui/dist/assets/CloudscapeSettings-UdgiI6dm.js
|
|
23
|
+
backend/../ui/dist/assets/CloudscapeSettings-CIXFvPKv.js
|
|
24
24
|
backend/../ui/dist/assets/CloudscapeShell-BXHVWjDE.css
|
|
25
|
-
backend/../ui/dist/assets/CloudscapeShell-
|
|
26
|
-
backend/../ui/dist/assets/StateMachineGraph-
|
|
27
|
-
backend/../ui/dist/assets/index-
|
|
28
|
-
backend/../ui/dist/assets/index-
|
|
29
|
-
backend/../ui/dist/assets/index-
|
|
30
|
-
backend/../ui/dist/assets/index-
|
|
25
|
+
backend/../ui/dist/assets/CloudscapeShell-DNfhSg0O.js
|
|
26
|
+
backend/../ui/dist/assets/StateMachineGraph-DvF_kIOO.js
|
|
27
|
+
backend/../ui/dist/assets/index-BC7E-Fm4.js
|
|
28
|
+
backend/../ui/dist/assets/index-BLdRlH9d.js
|
|
29
|
+
backend/../ui/dist/assets/index-Ba-oOcd3.js
|
|
30
|
+
backend/../ui/dist/assets/index-BkSgVQ0X.js
|
|
31
|
+
backend/../ui/dist/assets/index-C5z3cir-.js
|
|
31
32
|
backend/../ui/dist/assets/index-DE--A2EE.css
|
|
32
33
|
backend/../ui/dist/assets/index-DKIdXFX0.css
|
|
33
34
|
backend/../ui/dist/assets/index-DNBdeKId.css
|
|
34
|
-
backend/../ui/dist/assets/index-DhD1oh4N.js
|
|
35
35
|
backend/../ui/dist/assets/index-DoY0HscW.css
|
|
36
36
|
backend/../ui/dist/assets/index-DrkkXAoG.css
|
|
37
|
+
backend/../ui/dist/assets/index-Dw6TGweb.js
|
|
37
38
|
backend/../ui/dist/assets/index-Vf0UFiCT.css
|
|
38
|
-
backend/../ui/dist/assets/index-hhBdqMqs.js
|
|
39
39
|
backend/../ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
|
|
40
40
|
backend/../ui/dist/assets/utils-Cfmwk2UT.js
|
|
41
41
|
backend/../ui/dist/aws-icons/acm.svg
|
|
@@ -148,27 +148,27 @@ ui/dist/favicon.png
|
|
|
148
148
|
ui/dist/favicon.svg
|
|
149
149
|
ui/dist/index.html
|
|
150
150
|
ui/dist/stackport-logo.svg
|
|
151
|
-
ui/dist/assets/CloudscapeAbout-
|
|
152
|
-
ui/dist/assets/CloudscapeDashboard-
|
|
153
|
-
ui/dist/assets/CloudscapeLearn-
|
|
151
|
+
ui/dist/assets/CloudscapeAbout-BRbsDskR.js
|
|
152
|
+
ui/dist/assets/CloudscapeDashboard-j2f4Elz2.js
|
|
153
|
+
ui/dist/assets/CloudscapeLearn-CVW3k3X0.js
|
|
154
|
+
ui/dist/assets/CloudscapeResourceBrowser-CgWdgn20.js
|
|
154
155
|
ui/dist/assets/CloudscapeResourceBrowser-CzwePG-V.css
|
|
155
|
-
ui/dist/assets/
|
|
156
|
-
ui/dist/assets/CloudscapeSettings-UdgiI6dm.js
|
|
156
|
+
ui/dist/assets/CloudscapeSettings-CIXFvPKv.js
|
|
157
157
|
ui/dist/assets/CloudscapeShell-BXHVWjDE.css
|
|
158
|
-
ui/dist/assets/CloudscapeShell-
|
|
159
|
-
ui/dist/assets/StateMachineGraph-
|
|
160
|
-
ui/dist/assets/index-
|
|
161
|
-
ui/dist/assets/index-
|
|
162
|
-
ui/dist/assets/index-
|
|
163
|
-
ui/dist/assets/index-
|
|
158
|
+
ui/dist/assets/CloudscapeShell-DNfhSg0O.js
|
|
159
|
+
ui/dist/assets/StateMachineGraph-DvF_kIOO.js
|
|
160
|
+
ui/dist/assets/index-BC7E-Fm4.js
|
|
161
|
+
ui/dist/assets/index-BLdRlH9d.js
|
|
162
|
+
ui/dist/assets/index-Ba-oOcd3.js
|
|
163
|
+
ui/dist/assets/index-BkSgVQ0X.js
|
|
164
|
+
ui/dist/assets/index-C5z3cir-.js
|
|
164
165
|
ui/dist/assets/index-DE--A2EE.css
|
|
165
166
|
ui/dist/assets/index-DKIdXFX0.css
|
|
166
167
|
ui/dist/assets/index-DNBdeKId.css
|
|
167
|
-
ui/dist/assets/index-DhD1oh4N.js
|
|
168
168
|
ui/dist/assets/index-DoY0HscW.css
|
|
169
169
|
ui/dist/assets/index-DrkkXAoG.css
|
|
170
|
+
ui/dist/assets/index-Dw6TGweb.js
|
|
170
171
|
ui/dist/assets/index-Vf0UFiCT.css
|
|
171
|
-
ui/dist/assets/index-hhBdqMqs.js
|
|
172
172
|
ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
|
|
173
173
|
ui/dist/assets/utils-Cfmwk2UT.js
|
|
174
174
|
ui/dist/aws-icons/acm.svg
|