@a9s/cli 0.0.1 → 0.1.0
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.
- package/README.md +167 -2
- package/dist/scripts/seed.js +310 -0
- package/dist/src/App.js +476 -0
- package/dist/src/adapters/ServiceAdapter.js +1 -0
- package/dist/src/adapters/capabilities/ActionCapability.js +1 -0
- package/dist/src/adapters/capabilities/DetailCapability.js +1 -0
- package/dist/src/adapters/capabilities/EditCapability.js +1 -0
- package/dist/src/adapters/capabilities/YankCapability.js +42 -0
- package/dist/src/adapters/capabilities/YankCapability.test.js +29 -0
- package/dist/src/components/AdvancedTextInput.js +200 -0
- package/dist/src/components/AdvancedTextInput.test.js +190 -0
- package/dist/src/components/AutocompleteInput.js +29 -0
- package/dist/src/components/DetailPanel.js +12 -0
- package/dist/src/components/DiffViewer.js +17 -0
- package/dist/src/components/ErrorStatePanel.js +5 -0
- package/dist/src/components/HUD.js +31 -0
- package/dist/src/components/HelpPanel.js +33 -0
- package/dist/src/components/ModeBar.js +43 -0
- package/dist/src/components/Table/index.js +109 -0
- package/dist/src/components/Table/widths.js +19 -0
- package/dist/src/components/TableSkeleton.js +25 -0
- package/dist/src/components/YankHelpPanel.js +43 -0
- package/dist/src/constants/commands.js +15 -0
- package/dist/src/constants/keybindings.js +530 -0
- package/dist/src/constants/keys.js +37 -0
- package/dist/src/features/AppMainView.integration.test.js +133 -0
- package/dist/src/features/AppMainView.js +95 -0
- package/dist/src/hooks/inputEvents.js +1 -0
- package/dist/src/hooks/mainInputScopes.js +68 -0
- package/dist/src/hooks/mainInputScopes.test.js +24 -0
- package/dist/src/hooks/useActionController.js +78 -0
- package/dist/src/hooks/useAppController.js +102 -0
- package/dist/src/hooks/useAppController.test.js +54 -0
- package/dist/src/hooks/useAppData.js +48 -0
- package/dist/src/hooks/useAwsContext.js +77 -0
- package/dist/src/hooks/useAwsProfiles.js +53 -0
- package/dist/src/hooks/useAwsRegions.js +105 -0
- package/dist/src/hooks/useCommandRouter.js +56 -0
- package/dist/src/hooks/useCommandRouter.test.js +27 -0
- package/dist/src/hooks/useDetailController.js +57 -0
- package/dist/src/hooks/useDetailController.test.js +32 -0
- package/dist/src/hooks/useHelpPanel.js +65 -0
- package/dist/src/hooks/useHierarchyState.js +39 -0
- package/dist/src/hooks/useInputEventProcessor.js +450 -0
- package/dist/src/hooks/useInputEventProcessor.test.js +174 -0
- package/dist/src/hooks/useKeyChord.js +83 -0
- package/dist/src/hooks/useMainInput.js +18 -0
- package/dist/src/hooks/useNavigation.js +47 -0
- package/dist/src/hooks/usePendingAction.js +8 -0
- package/dist/src/hooks/usePickerManager.js +130 -0
- package/dist/src/hooks/usePickerState.js +47 -0
- package/dist/src/hooks/usePickerTable.js +20 -0
- package/dist/src/hooks/useServiceView.js +226 -0
- package/dist/src/hooks/useUiHints.js +60 -0
- package/dist/src/hooks/useYankMode.js +24 -0
- package/dist/src/hooks/yankHeaderMarkers.js +23 -0
- package/dist/src/hooks/yankHeaderMarkers.test.js +49 -0
- package/dist/src/index.js +30 -0
- package/dist/src/services.js +12 -0
- package/dist/src/state/atoms.js +27 -0
- package/dist/src/types.js +12 -0
- package/dist/src/utils/aws.js +39 -0
- package/dist/src/utils/debugLogger.js +34 -0
- package/dist/src/utils/secretDisplay.js +45 -0
- package/dist/src/utils/withFullscreen.js +38 -0
- package/dist/src/views/dynamodb/adapter.js +22 -0
- package/dist/src/views/iam/adapter.js +258 -0
- package/dist/src/views/iam/capabilities/detailCapability.js +93 -0
- package/dist/src/views/iam/capabilities/editCapability.js +59 -0
- package/dist/src/views/iam/capabilities/yankCapability.js +6 -0
- package/dist/src/views/iam/capabilities/yankOptions.js +15 -0
- package/dist/src/views/iam/schema.js +7 -0
- package/dist/src/views/iam/types.js +1 -0
- package/dist/src/views/iam/utils.js +21 -0
- package/dist/src/views/route53/adapter.js +22 -0
- package/dist/src/views/s3/adapter.js +154 -0
- package/dist/src/views/s3/capabilities/actionCapability.js +172 -0
- package/dist/src/views/s3/capabilities/detailCapability.js +115 -0
- package/dist/src/views/s3/capabilities/editCapability.js +35 -0
- package/dist/src/views/s3/capabilities/yankCapability.js +6 -0
- package/dist/src/views/s3/capabilities/yankOptions.js +55 -0
- package/dist/src/views/s3/client.js +12 -0
- package/dist/src/views/s3/fetcher.js +86 -0
- package/dist/src/views/s3/schema.js +6 -0
- package/dist/src/views/s3/utils.js +19 -0
- package/dist/src/views/secretsmanager/adapter.js +188 -0
- package/dist/src/views/secretsmanager/capabilities/actionCapability.js +193 -0
- package/dist/src/views/secretsmanager/capabilities/detailCapability.js +46 -0
- package/dist/src/views/secretsmanager/capabilities/editCapability.js +116 -0
- package/dist/src/views/secretsmanager/capabilities/yankCapability.js +7 -0
- package/dist/src/views/secretsmanager/capabilities/yankOptions.js +68 -0
- package/dist/src/views/secretsmanager/schema.js +28 -0
- package/dist/src/views/secretsmanager/types.js +1 -0
- package/package.json +68 -5
- package/index.js +0 -1
package/README.md
CHANGED
|
@@ -1,3 +1,168 @@
|
|
|
1
|
-
#
|
|
1
|
+
# a9s
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
  
|
|
4
|
+
|
|
5
|
+
**k9s-style TUI navigator for AWS services.** Inspired by [k9s](https://github.com/derailed/k9s).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### Global install (recommended)
|
|
10
|
+
|
|
11
|
+
```shell
|
|
12
|
+
npm install -g @a9s/cli
|
|
13
|
+
a9s
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Local install + npx
|
|
17
|
+
|
|
18
|
+
```shell
|
|
19
|
+
npm install @a9s/cli
|
|
20
|
+
npx @a9s/cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Launch the TUI:
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
a9s
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Navigation
|
|
32
|
+
|
|
33
|
+
- **Arrow keys / hjkl**: Navigate between rows
|
|
34
|
+
- **Tab**: Switch between columns (sort/filter)
|
|
35
|
+
- **Enter**: Drill into details or navigate to the next level
|
|
36
|
+
- **Backspace**: Go back to the previous level
|
|
37
|
+
- **/** (slash): Search/filter current view
|
|
38
|
+
- **:**: Command mode (e.g., `:services` to list available services)
|
|
39
|
+
- **?**: Show help panel with all keybindings for current context
|
|
40
|
+
|
|
41
|
+
### Service Switching
|
|
42
|
+
|
|
43
|
+
Press `:services` to see the list of available AWS services and switch between them.
|
|
44
|
+
|
|
45
|
+
### Common Operations
|
|
46
|
+
|
|
47
|
+
- **d**: Open detail panel for selected row (shows metadata like ARN, tags, etc.)
|
|
48
|
+
- **y + key**: Yank/copy shortcuts:
|
|
49
|
+
- `y+n` → copy name
|
|
50
|
+
- `y+a` → copy ARN
|
|
51
|
+
- `y+k` → copy S3 key or other identifiers
|
|
52
|
+
- **f**: Fetch/download S3 objects to local path
|
|
53
|
+
- **e**: Edit and upload (opens selected item in `$EDITOR`)
|
|
54
|
+
- **v**: Toggle reveal/hide secrets (Secrets Manager)
|
|
55
|
+
|
|
56
|
+
## Services Supported
|
|
57
|
+
|
|
58
|
+
| Service | Status | Features |
|
|
59
|
+
| --------------- | ------ | ----------------------------------------------- |
|
|
60
|
+
| S3 | ✅ | Browse buckets, objects, download, edit, delete |
|
|
61
|
+
| IAM | ✅ | List users, roles, policies |
|
|
62
|
+
| Route 53 | ✅ | List hosted zones, records |
|
|
63
|
+
| Secrets Manager | ✅ | View, edit, and upload secrets |
|
|
64
|
+
| DynamoDB | ✅ | List tables, view items |
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
- **Responsive tables** with sortable columns
|
|
69
|
+
- **Service switching** with `:services` command
|
|
70
|
+
- **VIM-inspired shortcuts** (hjkl navigation, commands)
|
|
71
|
+
- **Yank mode** for quick copy operations
|
|
72
|
+
- **Detail panels** showing rich metadata
|
|
73
|
+
- **In-editor editing** with upload confirmation
|
|
74
|
+
- **Search/filter** with `/` key
|
|
75
|
+
- **Help system** with context-sensitive keybindings
|
|
76
|
+
- **LocalStack support** for offline development
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
### Prerequisites
|
|
81
|
+
|
|
82
|
+
- Node.js 18+
|
|
83
|
+
- pnpm (for package management)
|
|
84
|
+
- Docker (for LocalStack)
|
|
85
|
+
|
|
86
|
+
### Setup
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pnpm install
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Run Against LocalStack
|
|
93
|
+
|
|
94
|
+
Start LocalStack + seed data:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pnpm localstack:setup
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Run the TUI (connects to LocalStack on port 4566):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pnpm dev:local
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Run Against AWS
|
|
107
|
+
|
|
108
|
+
Connect to your AWS account:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pnpm dev
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This will use your `~/.aws/credentials` and `AWS_REGION` environment variable.
|
|
115
|
+
|
|
116
|
+
### Testing
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pnpm test # Run tests
|
|
120
|
+
pnpm typecheck # Type checking
|
|
121
|
+
pnpm build # Build TypeScript to dist/
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Project Structure
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
src/
|
|
128
|
+
index.tsx - CLI entry point (commander)
|
|
129
|
+
App.tsx - Main TUI state machine & layout
|
|
130
|
+
types.ts - Core types (ColumnDef, TableRow, etc.)
|
|
131
|
+
services.ts - Service registry
|
|
132
|
+
adapters/ - ServiceAdapter implementations for each AWS service
|
|
133
|
+
views/ - Service-specific views (s3, iam, route53, dynamodb, secretsmanager)
|
|
134
|
+
components/ - Ink/React components (Table, HUD, DetailPanel, etc.)
|
|
135
|
+
hooks/ - Custom React hooks (navigation, state, etc.)
|
|
136
|
+
constants/ - Keybindings, commands
|
|
137
|
+
scripts/
|
|
138
|
+
seed.ts - LocalStack test data seeding
|
|
139
|
+
docker/
|
|
140
|
+
docker-compose.yml - LocalStack with services
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Goals
|
|
144
|
+
|
|
145
|
+
### Services (Planned)
|
|
146
|
+
|
|
147
|
+
- [x] S3
|
|
148
|
+
- [x] IAM
|
|
149
|
+
- [x] Route 53
|
|
150
|
+
- [x] DynamoDB
|
|
151
|
+
- [x] Secrets Manager
|
|
152
|
+
- [ ] EC2
|
|
153
|
+
- [ ] ELB
|
|
154
|
+
- [ ] CloudFront
|
|
155
|
+
|
|
156
|
+
### Features (Planned)
|
|
157
|
+
|
|
158
|
+
- [x] Responsive tables
|
|
159
|
+
- [x] Service switching
|
|
160
|
+
- [x] VIM shortcuts
|
|
161
|
+
- [x] Yank operations
|
|
162
|
+
- [x] Detail panels
|
|
163
|
+
- [x] Edit & upload
|
|
164
|
+
- [ ] Smart cross-service navigation (e.g., Route53 → ELB)
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { S3Client, CreateBucketCommand, PutObjectCommand, ListBucketsCommand, } from "@aws-sdk/client-s3";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const client = new S3Client({
|
|
6
|
+
endpoint: "http://localhost:4566",
|
|
7
|
+
forcePathStyle: true,
|
|
8
|
+
region: "us-east-1",
|
|
9
|
+
credentials: { accessKeyId: "test", secretAccessKey: "test" },
|
|
10
|
+
});
|
|
11
|
+
const BUCKETS = [
|
|
12
|
+
"my-app-logs",
|
|
13
|
+
"data-warehouse",
|
|
14
|
+
"static-assets",
|
|
15
|
+
"backups-prod",
|
|
16
|
+
"ml-datasets",
|
|
17
|
+
"user-uploads",
|
|
18
|
+
"audit-trail",
|
|
19
|
+
"terraform-state",
|
|
20
|
+
"build-artifacts",
|
|
21
|
+
"media-storage",
|
|
22
|
+
];
|
|
23
|
+
const OBJECT_TEMPLATES = [
|
|
24
|
+
"logs/{year}/{month}/app.log",
|
|
25
|
+
"logs/{year}/{month}/error.log",
|
|
26
|
+
"data/users.json",
|
|
27
|
+
"data/products.json",
|
|
28
|
+
"data/{year}/export.csv",
|
|
29
|
+
"config/settings.yaml",
|
|
30
|
+
"config/secrets.json.enc",
|
|
31
|
+
"reports/{year}/{month}/summary.pdf",
|
|
32
|
+
"images/avatars/{id}.png",
|
|
33
|
+
"images/banners/hero.jpg",
|
|
34
|
+
"scripts/deploy.sh",
|
|
35
|
+
"scripts/migrate.py",
|
|
36
|
+
"archive/{year}/backup.tar.gz",
|
|
37
|
+
"tmp/processing/{id}.tmp",
|
|
38
|
+
"public/index.html",
|
|
39
|
+
"public/styles.css",
|
|
40
|
+
"public/bundle.js",
|
|
41
|
+
];
|
|
42
|
+
function randomInt(min, max) {
|
|
43
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
44
|
+
}
|
|
45
|
+
function renderTemplate(template) {
|
|
46
|
+
return template
|
|
47
|
+
.replace("{year}", String(randomInt(2022, 2024)))
|
|
48
|
+
.replace("{month}", String(randomInt(1, 12)).padStart(2, "0"))
|
|
49
|
+
.replace("{id}", Math.random().toString(36).slice(2, 10));
|
|
50
|
+
}
|
|
51
|
+
async function ensureBucket(name) {
|
|
52
|
+
try {
|
|
53
|
+
await client.send(new CreateBucketCommand({ Bucket: name }));
|
|
54
|
+
console.log(` Created bucket: ${name}`);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
const err = e;
|
|
58
|
+
if (err.name === "BucketAlreadyOwnedByYou" || err.name === "BucketAlreadyExists") {
|
|
59
|
+
console.log(` Bucket already exists: ${name}`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function seedBucket(bucket) {
|
|
67
|
+
const count = randomInt(0, 30);
|
|
68
|
+
const templates = [...OBJECT_TEMPLATES].sort(() => Math.random() - 0.5).slice(0, count);
|
|
69
|
+
for (const tpl of templates) {
|
|
70
|
+
const key = renderTemplate(tpl);
|
|
71
|
+
const content = `Seeded content for ${key} in ${bucket}\n`;
|
|
72
|
+
await client.send(new PutObjectCommand({
|
|
73
|
+
Bucket: bucket,
|
|
74
|
+
Key: key,
|
|
75
|
+
Body: content,
|
|
76
|
+
ContentType: "text/plain",
|
|
77
|
+
}));
|
|
78
|
+
process.stdout.write(".");
|
|
79
|
+
}
|
|
80
|
+
if (count > 0)
|
|
81
|
+
console.log(` ${count} objects`);
|
|
82
|
+
else
|
|
83
|
+
console.log(" (empty)");
|
|
84
|
+
}
|
|
85
|
+
async function checkLocalStack() {
|
|
86
|
+
try {
|
|
87
|
+
await client.send(new ListBucketsCommand({}));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
console.error("Cannot connect to LocalStack at http://localhost:4566\n" +
|
|
91
|
+
"Run: pnpm localstack:up\n" +
|
|
92
|
+
"Wait a few seconds for it to start, then retry.");
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function runAws(args) {
|
|
97
|
+
const env = {
|
|
98
|
+
...process.env,
|
|
99
|
+
AWS_ACCESS_KEY_ID: "test",
|
|
100
|
+
AWS_SECRET_ACCESS_KEY: "test",
|
|
101
|
+
AWS_DEFAULT_REGION: "us-east-1",
|
|
102
|
+
};
|
|
103
|
+
const { stdout } = await execFileAsync("aws", ["--endpoint-url", "http://localhost:4566", ...args], {
|
|
104
|
+
env,
|
|
105
|
+
timeout: 5000,
|
|
106
|
+
});
|
|
107
|
+
return stdout;
|
|
108
|
+
}
|
|
109
|
+
async function ensureManagedPolicy(policyName, policyDocument) {
|
|
110
|
+
const listOut = await runAws([
|
|
111
|
+
"iam",
|
|
112
|
+
"list-policies",
|
|
113
|
+
"--scope",
|
|
114
|
+
"Local",
|
|
115
|
+
"--query",
|
|
116
|
+
`Policies[?PolicyName=='${policyName}'].Arn | [0]`,
|
|
117
|
+
"--output",
|
|
118
|
+
"text",
|
|
119
|
+
]);
|
|
120
|
+
const existingArn = listOut.trim();
|
|
121
|
+
if (existingArn && existingArn !== "None") {
|
|
122
|
+
console.log(` Managed policy already exists: ${policyName}`);
|
|
123
|
+
return existingArn;
|
|
124
|
+
}
|
|
125
|
+
const createOut = await runAws([
|
|
126
|
+
"iam",
|
|
127
|
+
"create-policy",
|
|
128
|
+
"--policy-name",
|
|
129
|
+
policyName,
|
|
130
|
+
"--policy-document",
|
|
131
|
+
JSON.stringify(policyDocument),
|
|
132
|
+
"--output",
|
|
133
|
+
"json",
|
|
134
|
+
]);
|
|
135
|
+
const parsed = JSON.parse(createOut);
|
|
136
|
+
const arn = parsed.Policy?.Arn;
|
|
137
|
+
if (!arn)
|
|
138
|
+
throw new Error(`Failed creating managed policy ${policyName}`);
|
|
139
|
+
console.log(` Created managed policy: ${policyName}`);
|
|
140
|
+
return arn;
|
|
141
|
+
}
|
|
142
|
+
async function ensureRole(roleName, trustPolicy) {
|
|
143
|
+
const existing = await runAws([
|
|
144
|
+
"iam",
|
|
145
|
+
"get-role",
|
|
146
|
+
"--role-name",
|
|
147
|
+
roleName,
|
|
148
|
+
"--query",
|
|
149
|
+
"Role.RoleName",
|
|
150
|
+
"--output",
|
|
151
|
+
"text",
|
|
152
|
+
]).catch(() => "");
|
|
153
|
+
if (existing.trim() === roleName) {
|
|
154
|
+
console.log(` Role already exists: ${roleName}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await runAws([
|
|
158
|
+
"iam",
|
|
159
|
+
"create-role",
|
|
160
|
+
"--role-name",
|
|
161
|
+
roleName,
|
|
162
|
+
"--assume-role-policy-document",
|
|
163
|
+
JSON.stringify(trustPolicy),
|
|
164
|
+
]);
|
|
165
|
+
console.log(` Created role: ${roleName}`);
|
|
166
|
+
}
|
|
167
|
+
async function ensureInlineRolePolicy(roleName, policyName, policyDocument) {
|
|
168
|
+
await runAws([
|
|
169
|
+
"iam",
|
|
170
|
+
"put-role-policy",
|
|
171
|
+
"--role-name",
|
|
172
|
+
roleName,
|
|
173
|
+
"--policy-name",
|
|
174
|
+
policyName,
|
|
175
|
+
"--policy-document",
|
|
176
|
+
JSON.stringify(policyDocument),
|
|
177
|
+
]);
|
|
178
|
+
console.log(` Put inline policy ${policyName} on ${roleName}`);
|
|
179
|
+
}
|
|
180
|
+
async function ensureAttachedRolePolicy(roleName, policyArn) {
|
|
181
|
+
const attached = await runAws([
|
|
182
|
+
"iam",
|
|
183
|
+
"list-attached-role-policies",
|
|
184
|
+
"--role-name",
|
|
185
|
+
roleName,
|
|
186
|
+
"--query",
|
|
187
|
+
`AttachedPolicies[?PolicyArn=='${policyArn}'].PolicyArn | [0]`,
|
|
188
|
+
"--output",
|
|
189
|
+
"text",
|
|
190
|
+
]);
|
|
191
|
+
if (attached.trim() === policyArn) {
|
|
192
|
+
console.log(` Managed policy already attached to ${roleName}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
await runAws(["iam", "attach-role-policy", "--role-name", roleName, "--policy-arn", policyArn]);
|
|
196
|
+
console.log(` Attached managed policy to ${roleName}`);
|
|
197
|
+
}
|
|
198
|
+
async function seedSecretsManager() {
|
|
199
|
+
console.log("\nSeeding Secrets Manager:");
|
|
200
|
+
const secrets = [
|
|
201
|
+
{ name: "app/db-password", value: "s3cr3tP@ssword123" },
|
|
202
|
+
{ name: "app/api-key", value: "sk-test-abc123xyz" },
|
|
203
|
+
{ name: "app/jwt-secret", value: "super-secret-jwt-key-do-not-share" },
|
|
204
|
+
{ name: "infra/config", value: JSON.stringify({ host: "localhost", port: 5432, db: "app" }) },
|
|
205
|
+
{
|
|
206
|
+
name: "prod/tls-cert",
|
|
207
|
+
value: "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----",
|
|
208
|
+
},
|
|
209
|
+
{ name: "monitoring/grafana-api-key", value: "glsa_test123456" },
|
|
210
|
+
];
|
|
211
|
+
for (const secret of secrets) {
|
|
212
|
+
try {
|
|
213
|
+
await runAws([
|
|
214
|
+
"secretsmanager",
|
|
215
|
+
"create-secret",
|
|
216
|
+
"--name",
|
|
217
|
+
secret.name,
|
|
218
|
+
"--secret-string",
|
|
219
|
+
secret.value,
|
|
220
|
+
]);
|
|
221
|
+
console.log(` Created secret: ${secret.name}`);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
const err = e;
|
|
225
|
+
if (err.message?.includes("ResourceExistsException")) {
|
|
226
|
+
console.log(` Secret already exists: ${secret.name}`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
throw e;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function seedIam() {
|
|
235
|
+
console.log("\nSeeding IAM:");
|
|
236
|
+
const trustPolicy = {
|
|
237
|
+
Version: "2012-10-17",
|
|
238
|
+
Statement: [
|
|
239
|
+
{
|
|
240
|
+
Effect: "Allow",
|
|
241
|
+
Principal: { Service: "ec2.amazonaws.com" },
|
|
242
|
+
Action: "sts:AssumeRole",
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
const appReadPolicy = {
|
|
247
|
+
Version: "2012-10-17",
|
|
248
|
+
Statement: [
|
|
249
|
+
{
|
|
250
|
+
Sid: "ReadSomeBuckets",
|
|
251
|
+
Effect: "Allow",
|
|
252
|
+
Action: ["s3:GetObject", "s3:ListBucket"],
|
|
253
|
+
Resource: [
|
|
254
|
+
"arn:aws:s3:::media-storage",
|
|
255
|
+
"arn:aws:s3:::media-storage/*",
|
|
256
|
+
"arn:aws:s3:::static-assets",
|
|
257
|
+
"arn:aws:s3:::static-assets/*",
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
const auditPolicy = {
|
|
263
|
+
Version: "2012-10-17",
|
|
264
|
+
Statement: [
|
|
265
|
+
{
|
|
266
|
+
Sid: "AuditReadOnly",
|
|
267
|
+
Effect: "Allow",
|
|
268
|
+
Action: ["s3:GetObject", "s3:ListBucket"],
|
|
269
|
+
Resource: ["arn:aws:s3:::audit-trail", "arn:aws:s3:::audit-trail/*"],
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
};
|
|
273
|
+
const inlineDeployPolicy = {
|
|
274
|
+
Version: "2012-10-17",
|
|
275
|
+
Statement: [
|
|
276
|
+
{
|
|
277
|
+
Sid: "WriteBuildArtifacts",
|
|
278
|
+
Effect: "Allow",
|
|
279
|
+
Action: ["s3:PutObject", "s3:DeleteObject", "s3:GetObject"],
|
|
280
|
+
Resource: ["arn:aws:s3:::build-artifacts/*"],
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
};
|
|
284
|
+
const readPolicyArn = await ensureManagedPolicy("A9SAppReadPolicy", appReadPolicy);
|
|
285
|
+
const auditPolicyArn = await ensureManagedPolicy("A9SAuditPolicy", auditPolicy);
|
|
286
|
+
await ensureRole("A9SAppRole", trustPolicy);
|
|
287
|
+
await ensureRole("A9SReadOnlyRole", trustPolicy);
|
|
288
|
+
await ensureInlineRolePolicy("A9SAppRole", "A9SInlineDeployPolicy", inlineDeployPolicy);
|
|
289
|
+
await ensureAttachedRolePolicy("A9SAppRole", readPolicyArn);
|
|
290
|
+
await ensureAttachedRolePolicy("A9SReadOnlyRole", auditPolicyArn);
|
|
291
|
+
}
|
|
292
|
+
async function main() {
|
|
293
|
+
console.log("Checking LocalStack connection...");
|
|
294
|
+
await checkLocalStack();
|
|
295
|
+
console.log("Connected!\n");
|
|
296
|
+
for (const bucket of BUCKETS) {
|
|
297
|
+
console.log(`Seeding ${bucket}:`);
|
|
298
|
+
await ensureBucket(bucket);
|
|
299
|
+
process.stdout.write(" Objects: ");
|
|
300
|
+
await seedBucket(bucket);
|
|
301
|
+
}
|
|
302
|
+
await seedIam();
|
|
303
|
+
await seedSecretsManager();
|
|
304
|
+
console.log("\nDone! LocalStack seeded with test data.");
|
|
305
|
+
console.log("Run: pnpm dev:local");
|
|
306
|
+
}
|
|
307
|
+
main().catch((e) => {
|
|
308
|
+
console.error(e);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
});
|