ramm 0.0.59 → 0.0.62
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 +298 -68
- package/package.json +6 -2
- package/dist/bun.sh +0 -23
- package/dist/ramm.js +0 -620
- package/dist/types/base/base.d.ts +0 -35
- package/dist/types/base/tee.d.ts +0 -2
- package/dist/types/build.d.ts +0 -7
- package/dist/types/context.d.ts +0 -16
- package/dist/types/cron.d.ts +0 -4
- package/dist/types/files.d.ts +0 -4
- package/dist/types/init.d.ts +0 -2
- package/dist/types/nft.d.ts +0 -6
- package/dist/types/packages.d.ts +0 -13
- package/dist/types/path.d.ts +0 -1
- package/dist/types/podman.d.ts +0 -25
- package/dist/types/print.d.ts +0 -3
- package/dist/types/ramm.d.ts +0 -13
- package/dist/types/ssh.d.ts +0 -11
- package/dist/types/systemd.d.ts +0 -10
package/README.md
CHANGED
|
@@ -1,151 +1,381 @@
|
|
|
1
|
-
|
|
1
|
+
# RAMM
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Bun library for server management and deployment automation in TypeScript. Write your infrastructure logic in real code — no YAML, no DSL, no templating language to fight with.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Why not Ansible?
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Ansible is great, but it comes with a cost: you write infrastructure in YAML with Jinja2 templates. The moment your logic gets non-trivial — conditionals, loops, dynamic values — you're fighting the format instead of solving the problem.
|
|
8
|
+
|
|
9
|
+
RAMM takes a different approach: **your deployment scripts are just TypeScript**. Full language, real abstractions, IDE support, type checking, any npm package you need. If you know JS, you already know how to write RAMM scripts.
|
|
10
|
+
|
|
11
|
+
The trade-off is explicit: Ansible gives you a huge module ecosystem and declarative guarantees. RAMM gives you simplicity and the full power of a real programming language. For developers who want to own their deployment code without learning a separate tool, RAMM is the better fit.
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
The core pattern is `buildAndRunOverSsh`: write a script that configures your server (`server.ts`), then run it from your local machine (`client.ts`). RAMM compiles the server script with `bun build` and executes it on the remote host — all commands inside run on the server.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
client.ts → [bun build] → server.ts runs on remote
|
|
19
|
+
(local machine) (all execCommand calls are local to the server)
|
|
20
|
+
```
|
|
11
21
|
|
|
12
22
|
## Installation
|
|
13
23
|
|
|
14
24
|
```bash
|
|
15
25
|
bun add ramm
|
|
16
|
-
# or
|
|
17
|
-
npm install ramm
|
|
18
26
|
```
|
|
19
27
|
|
|
20
|
-
## Quick Start
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
**`client.ts`** — runs on your local machine:
|
|
21
31
|
|
|
22
32
|
```ts
|
|
23
|
-
import {
|
|
24
|
-
import { Context, exec, copyFiles, debug, installBun } from "ramm";
|
|
33
|
+
import { buildAndRunOverSsh, Context } from "ramm";
|
|
25
34
|
|
|
26
|
-
const
|
|
35
|
+
const ctx = new Context({ user: "root", address: "1.2.3.4" });
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
await buildAndRunOverSsh({
|
|
38
|
+
entrypoint: "./server.ts",
|
|
39
|
+
context: ctx,
|
|
40
|
+
});
|
|
41
|
+
```
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
**`server.ts`** — compiled and executed on the remote server:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import {
|
|
47
|
+
installPodman,
|
|
48
|
+
createPodmanCommand,
|
|
49
|
+
runPodmanContainerService,
|
|
50
|
+
setupNftable,
|
|
51
|
+
printBlock,
|
|
52
|
+
} from "ramm";
|
|
53
|
+
|
|
54
|
+
printBlock("Firewall");
|
|
55
|
+
await setupNftable({
|
|
56
|
+
allowedIpV4: ["1.2.3.4"],
|
|
57
|
+
allowedPorts: [443],
|
|
36
58
|
});
|
|
37
59
|
|
|
38
|
-
|
|
39
|
-
await
|
|
60
|
+
printBlock("Container");
|
|
61
|
+
await installPodman();
|
|
62
|
+
|
|
63
|
+
const cmd = createPodmanCommand({
|
|
64
|
+
name: "app",
|
|
65
|
+
command: "myregistry.io/app:latest",
|
|
66
|
+
envs: [{ name: "PORT", value: "3000" }],
|
|
67
|
+
});
|
|
40
68
|
|
|
41
|
-
|
|
42
|
-
await exec(context, "bun run ./dist/server.js");
|
|
69
|
+
await runPodmanContainerService("app", cmd);
|
|
43
70
|
```
|
|
44
71
|
|
|
45
|
-
|
|
72
|
+
---
|
|
46
73
|
|
|
47
|
-
|
|
74
|
+
## API
|
|
48
75
|
|
|
49
|
-
|
|
76
|
+
### Context
|
|
77
|
+
|
|
78
|
+
Describes the server connection.
|
|
50
79
|
|
|
51
80
|
```ts
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
81
|
+
const ctx = new Context({
|
|
82
|
+
user: "root",
|
|
83
|
+
address: "1.2.3.4",
|
|
84
|
+
sshKey: "~/.ssh/id_ed25519", // optional
|
|
85
|
+
sudo: false, // prefix commands with sudo (default: false)
|
|
86
|
+
userspace: false, // use systemd --user scope (default: false)
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
ctx.getAddress(); // "root@1.2.3.4"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### Commands
|
|
95
|
+
|
|
96
|
+
#### `execCommand(command, props?, context?)`
|
|
97
|
+
|
|
98
|
+
Runs a command locally. Throws on non-zero exit code.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
await execCommand("systemctl restart app");
|
|
102
|
+
await execCommand("mkdir -p /opt/app", {}, ctx); // with sudo if ctx.sudo = true
|
|
56
103
|
```
|
|
57
104
|
|
|
58
|
-
|
|
59
|
-
- address: Server IP/hostname
|
|
60
|
-
- getAddress(): Returns "user@host" format
|
|
105
|
+
#### `execCommandMayError(command, props?, context?)`
|
|
61
106
|
|
|
62
|
-
|
|
107
|
+
Same as `execCommand` but does not throw — returns the result with exit code.
|
|
63
108
|
|
|
64
109
|
```ts
|
|
65
|
-
|
|
110
|
+
const result = await execCommandMayError("command -v podman", {}, ctx);
|
|
111
|
+
if (result.spawnResult.exitCode !== 0) {
|
|
112
|
+
// podman is not installed
|
|
113
|
+
}
|
|
66
114
|
```
|
|
67
115
|
|
|
68
|
-
|
|
116
|
+
#### `execCommandOverSsh(command, context)`
|
|
69
117
|
|
|
70
|
-
|
|
118
|
+
Runs a command on a remote server over SSH.
|
|
71
119
|
|
|
72
120
|
```ts
|
|
73
|
-
await
|
|
121
|
+
await execCommandOverSsh("systemctl restart app", ctx);
|
|
74
122
|
```
|
|
75
123
|
|
|
76
|
-
|
|
124
|
+
#### `copyFilesOverSsh(from, to, context)`
|
|
125
|
+
|
|
126
|
+
Copies files to the remote server using rsync.
|
|
77
127
|
|
|
78
128
|
```ts
|
|
79
|
-
|
|
129
|
+
await copyFilesOverSsh("./dist/", "/opt/app/dist", ctx);
|
|
80
130
|
```
|
|
81
131
|
|
|
82
|
-
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### Build & Deploy
|
|
83
135
|
|
|
84
|
-
|
|
136
|
+
#### `buildAndRunOverSsh({ entrypoint, context })`
|
|
137
|
+
|
|
138
|
+
Compiles a TypeScript script with `bun build` and runs it on the remote server. This is the primary usage pattern.
|
|
85
139
|
|
|
86
140
|
```ts
|
|
87
|
-
await
|
|
141
|
+
await buildAndRunOverSsh({
|
|
142
|
+
entrypoint: "./server.ts",
|
|
143
|
+
context: ctx,
|
|
144
|
+
});
|
|
88
145
|
```
|
|
89
146
|
|
|
90
|
-
|
|
147
|
+
#### `passVarsClient(data, context)` / `passVarsServer(context?)`
|
|
148
|
+
|
|
149
|
+
Passes variables from the local environment into the server script. Call `passVarsClient` before `buildAndRunOverSsh`, then `passVarsServer` inside the server script.
|
|
91
150
|
|
|
92
151
|
```ts
|
|
93
|
-
|
|
152
|
+
// client.ts
|
|
153
|
+
await passVarsClient({ DB_URL: process.env.DB_URL }, ctx);
|
|
154
|
+
await buildAndRunOverSsh({ entrypoint: "./server.ts", context: ctx });
|
|
155
|
+
|
|
156
|
+
// server.ts
|
|
157
|
+
const vars = await passVarsServer();
|
|
158
|
+
console.log(vars.DB_URL);
|
|
94
159
|
```
|
|
95
160
|
|
|
96
|
-
|
|
161
|
+
---
|
|
97
162
|
|
|
98
|
-
|
|
163
|
+
### Packages
|
|
164
|
+
|
|
165
|
+
#### `installSystemPackage(package, context?)`
|
|
166
|
+
|
|
167
|
+
Installs a system package. Auto-detects the OS and uses `apt` or `dnf`. Skips if already installed.
|
|
99
168
|
|
|
100
169
|
```ts
|
|
101
|
-
await installSystemPackage("
|
|
170
|
+
await installSystemPackage("nftables");
|
|
171
|
+
|
|
172
|
+
// with explicit config for a custom package
|
|
173
|
+
await installSystemPackage({
|
|
174
|
+
name: "nginx",
|
|
175
|
+
command: "nginx",
|
|
176
|
+
});
|
|
102
177
|
```
|
|
103
178
|
|
|
104
|
-
|
|
179
|
+
#### `installBunOverSsh(context)`
|
|
180
|
+
|
|
181
|
+
Installs Bun on a remote server.
|
|
105
182
|
|
|
106
183
|
```ts
|
|
107
|
-
|
|
184
|
+
await installBunOverSsh(ctx);
|
|
108
185
|
```
|
|
109
186
|
|
|
110
|
-
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### Podman
|
|
111
190
|
|
|
112
|
-
|
|
191
|
+
#### `installPodman(context?)`
|
|
192
|
+
|
|
193
|
+
Installs Podman and creates the `ramm` network. Skips if already installed.
|
|
113
194
|
|
|
114
195
|
```ts
|
|
115
196
|
await installPodman();
|
|
116
197
|
```
|
|
117
198
|
|
|
118
|
-
|
|
199
|
+
#### `createPodmanCommand(options)`
|
|
200
|
+
|
|
201
|
+
Builds a `podman run` command string from structured options.
|
|
119
202
|
|
|
120
203
|
```ts
|
|
121
|
-
|
|
204
|
+
const cmd = createPodmanCommand({
|
|
205
|
+
name: "app",
|
|
206
|
+
command: "myregistry.io/app:latest",
|
|
207
|
+
networks: ["ramm"], // default: ["ramm"]
|
|
208
|
+
replace: true, // default: true
|
|
209
|
+
background: true, // default: true
|
|
210
|
+
envs: [{ name: "PORT", value: "3000" }],
|
|
211
|
+
volumes: [{ from: "/data", to: "/data" }],
|
|
212
|
+
});
|
|
213
|
+
// "podman run --name app --replace -d --network ramm -e PORT=3000 -v /data:/data myregistry.io/app:latest"
|
|
122
214
|
```
|
|
123
215
|
|
|
124
|
-
|
|
216
|
+
#### `runPodmanContainer(name, command, context?)`
|
|
125
217
|
|
|
126
|
-
|
|
127
|
-
- Recreates if configuration changed
|
|
128
|
-
- Skips if already running
|
|
218
|
+
Runs a container. Skips if already running with the same command. Recreates if the command changed.
|
|
129
219
|
|
|
130
|
-
|
|
220
|
+
```ts
|
|
221
|
+
await runPodmanContainer("app", cmd);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### `runPodmanContainerService(name, command, context?)`
|
|
225
|
+
|
|
226
|
+
Runs a container and registers it as a systemd service for autostart on reboot.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
await runPodmanContainerService("app", cmd);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### `loginPodman(address, login, password, context?)`
|
|
233
|
+
|
|
234
|
+
Authenticates with a container registry.
|
|
131
235
|
|
|
132
236
|
```ts
|
|
133
|
-
await
|
|
237
|
+
await loginPodman("registry.example.com", "user", "password");
|
|
134
238
|
```
|
|
135
239
|
|
|
136
|
-
|
|
240
|
+
#### `addNftPodmanRule(context?)`
|
|
241
|
+
|
|
242
|
+
Adds an nftables rule to allow Podman network traffic through the firewall.
|
|
137
243
|
|
|
138
244
|
```ts
|
|
139
|
-
|
|
245
|
+
await addNftPodmanRule();
|
|
140
246
|
```
|
|
141
247
|
|
|
142
|
-
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### Systemd
|
|
251
|
+
|
|
252
|
+
#### `createSystemdService(name, content, context?)`
|
|
253
|
+
|
|
254
|
+
Writes a unit file, enables and starts the service.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
await createSystemdService(
|
|
258
|
+
"app.service",
|
|
259
|
+
`[Unit]
|
|
260
|
+
Description=My App
|
|
261
|
+
|
|
262
|
+
[Service]
|
|
263
|
+
ExecStart=bun run /opt/app/server.js
|
|
264
|
+
Restart=always
|
|
265
|
+
|
|
266
|
+
[Install]
|
|
267
|
+
WantedBy=multi-user.target`
|
|
268
|
+
);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### `createSystemdUnit(name, content, context?)`
|
|
272
|
+
|
|
273
|
+
Writes a unit file and reloads systemd. Does not start the service.
|
|
274
|
+
|
|
275
|
+
#### `startSystemdUnit(name, context?)` / `restartSystemdUnit(name, context?)` / `enableSystemdUnit(name, context?)` / `reloadSystemd(context?)`
|
|
276
|
+
|
|
277
|
+
Systemd unit management.
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
await reloadSystemd();
|
|
281
|
+
await enableSystemdUnit("app.service");
|
|
282
|
+
await startSystemdUnit("app.service");
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### `getSystemdPathToUnit(name, context?)`
|
|
286
|
+
|
|
287
|
+
Returns the path to the unit file — `/etc/systemd/system/` or `~/.config/systemd/user/` for userspace context.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### Firewall (nftables)
|
|
292
|
+
|
|
293
|
+
#### `setupNftable({ allowedIpV4, allowedPorts, context? })`
|
|
294
|
+
|
|
295
|
+
Creates an `inet ramm` nftables table. Closes all ports except the ones listed, rate-limits SSH, saves the config, and enables autostart.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
await setupNftable({
|
|
299
|
+
allowedIpV4: ["1.2.3.4"], // IPs with unrestricted SSH access
|
|
300
|
+
allowedPorts: [80, 443],
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
### SSH Keys
|
|
307
|
+
|
|
308
|
+
#### `createAndAddSshKey(filePath, comment, context)`
|
|
309
|
+
|
|
310
|
+
Creates an ed25519 key (if it doesn't exist) and adds the public key to `authorized_keys` on the server.
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
await createAndAddSshKey("~/.ssh/deploy_key", "deploy", ctx);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### `addSshKeyToUse({ key, fingerprint, filePath, server, context? })`
|
|
317
|
+
|
|
318
|
+
Saves a private key locally, adds the fingerprint to `known_hosts`, and configures `~/.ssh/config`.
|
|
319
|
+
|
|
320
|
+
#### `saveSshFingerptint(filePath, context)`
|
|
321
|
+
|
|
322
|
+
Fetches the server's fingerprint and saves it to a file.
|
|
323
|
+
|
|
324
|
+
#### `addKeyToHostConfig(pathToHost, address, pathToKey, context?)`
|
|
325
|
+
|
|
326
|
+
Adds a `Host` block to the SSH config.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
### Files
|
|
331
|
+
|
|
332
|
+
#### `writeFile(path, content, context?)`
|
|
333
|
+
|
|
334
|
+
Writes a file. Skips if the content is already the same.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
await writeFile("/etc/app/config.json", JSON.stringify(config));
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
#### `writeFileStrUniq(path, str, context?)`
|
|
341
|
+
|
|
342
|
+
Appends a string to a file only if it is not already present.
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
await writeFileStrUniq("~/.bashrc", 'export PATH="$PATH:/opt/bin"');
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
### Cron
|
|
351
|
+
|
|
352
|
+
#### `createCron({ time, pathToFile, context? })`
|
|
353
|
+
|
|
354
|
+
Adds a crontab entry. Skips if the entry already exists. Updates the schedule if the file is already in crontab with a different time.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
await createCron({
|
|
358
|
+
time: "0 3 * * *",
|
|
359
|
+
pathToFile: "/opt/scripts/backup.sh",
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
### Utilities
|
|
366
|
+
|
|
367
|
+
#### `printBlock(name)`
|
|
368
|
+
|
|
369
|
+
Prints a named block to the console — useful for structuring deployment output.
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
printBlock("Database"); // → Block: Database
|
|
373
|
+
```
|
|
143
374
|
|
|
144
|
-
|
|
145
|
-
- Executes bun.sh on remote host
|
|
375
|
+
#### `normalizePath(path)`
|
|
146
376
|
|
|
147
|
-
|
|
377
|
+
Expands `~/` to an absolute path.
|
|
148
378
|
|
|
149
379
|
```ts
|
|
150
|
-
|
|
380
|
+
normalizePath("~/.ssh/config"); // "/home/user/.ssh/config"
|
|
151
381
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ramm",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.62",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "bun build ./src/ramm.ts --target bun --outdir ./dist && cp ./src/bun.sh ./dist/bun.sh",
|
|
7
7
|
"types": "tsc --project tsconfig.types.json"
|
|
@@ -14,9 +14,13 @@
|
|
|
14
14
|
"engines": {
|
|
15
15
|
"bun": ">=1.0.0"
|
|
16
16
|
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/utftu/ramm.git"
|
|
20
|
+
},
|
|
17
21
|
"devDependencies": {
|
|
18
22
|
"@types/bun": "latest",
|
|
19
23
|
"typescript": "^5.8.2",
|
|
20
24
|
"dapes": "^0.0.26"
|
|
21
25
|
}
|
|
22
|
-
}
|
|
26
|
+
}
|
package/dist/bun.sh
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
if ! command -v unzip &> /dev/null; then
|
|
4
|
-
. /etc/os-release
|
|
5
|
-
|
|
6
|
-
if [[ "$ID_LIKE" == *"rhel"* ]]; then
|
|
7
|
-
sudo dnf install -y unzip
|
|
8
|
-
fi
|
|
9
|
-
|
|
10
|
-
if [[ "$ID" == "ubuntu" ]]; then
|
|
11
|
-
sudo apt update
|
|
12
|
-
sudo apt install -y unzip
|
|
13
|
-
fi
|
|
14
|
-
fi
|
|
15
|
-
|
|
16
|
-
if ! command -v bun &> /dev/null; then
|
|
17
|
-
echo "Installing bun..."
|
|
18
|
-
|
|
19
|
-
curl -fsSL https://bun.sh/install | bash
|
|
20
|
-
ln -s "$HOME/.bun/bin/bun" /usr/local/bin/bun
|
|
21
|
-
else
|
|
22
|
-
echo "Already installed bun."
|
|
23
|
-
fi
|
package/dist/ramm.js
DELETED
|
@@ -1,620 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/base/base.ts
|
|
3
|
-
var {spawn } = globalThis.Bun;
|
|
4
|
-
|
|
5
|
-
// src/print.ts
|
|
6
|
-
var printInternal = (left, right) => {
|
|
7
|
-
console.info(`\x1B[32m${left}:\x1B[0m \x1B[38;5;85m${right}\x1B[0m`);
|
|
8
|
-
};
|
|
9
|
-
var printCommand = (command) => {
|
|
10
|
-
printInternal("Command", command);
|
|
11
|
-
};
|
|
12
|
-
var printFunction = (func) => {
|
|
13
|
-
printInternal("Function", func);
|
|
14
|
-
};
|
|
15
|
-
var printBlock = (name) => {
|
|
16
|
-
console.info(`\x1B[34mBlock:\x1B[0m \x1B[38;5;81m${name}\x1B[0m`);
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// src/context.ts
|
|
20
|
-
class Context {
|
|
21
|
-
user;
|
|
22
|
-
domain;
|
|
23
|
-
userspace;
|
|
24
|
-
sudo;
|
|
25
|
-
sshKey;
|
|
26
|
-
params = {};
|
|
27
|
-
constructor({
|
|
28
|
-
user,
|
|
29
|
-
address,
|
|
30
|
-
userspace = false,
|
|
31
|
-
sudo = false,
|
|
32
|
-
sshKey
|
|
33
|
-
}) {
|
|
34
|
-
this.user = user;
|
|
35
|
-
this.domain = address;
|
|
36
|
-
this.sudo = sudo;
|
|
37
|
-
this.userspace = userspace;
|
|
38
|
-
this.sshKey = sshKey;
|
|
39
|
-
}
|
|
40
|
-
getAddress() {
|
|
41
|
-
return `${this.user}@${this.domain}`;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// src/base/tee.ts
|
|
46
|
-
var tee = async (read, write, prefix) => {
|
|
47
|
-
const reader = read.getReader();
|
|
48
|
-
let leftover = "";
|
|
49
|
-
let output = "";
|
|
50
|
-
const decoder = new TextDecoder("utf-8");
|
|
51
|
-
while (true) {
|
|
52
|
-
const { value, done } = await reader.read();
|
|
53
|
-
if (done) {
|
|
54
|
-
if (leftover)
|
|
55
|
-
write(prefix + leftover + `
|
|
56
|
-
`);
|
|
57
|
-
return output;
|
|
58
|
-
}
|
|
59
|
-
const decodedOutput = decoder.decode(value, { stream: true });
|
|
60
|
-
output += decodedOutput;
|
|
61
|
-
const lines = (leftover + decodedOutput).split(`
|
|
62
|
-
`);
|
|
63
|
-
leftover = lines.pop() ?? "";
|
|
64
|
-
for (const line of lines) {
|
|
65
|
-
write(prefix + line + `
|
|
66
|
-
`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
var teeStdout = (read, prefix) => {
|
|
71
|
-
return tee(read, (text) => process.stdout.write(text), prefix);
|
|
72
|
-
};
|
|
73
|
-
var teeStderr = (read, prefix) => {
|
|
74
|
-
return tee(read, (text) => process.stderr.write(text), prefix);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// src/base/base.ts
|
|
78
|
-
var defaultContext = new Context({
|
|
79
|
-
user: "root",
|
|
80
|
-
address: "0.0.0.0",
|
|
81
|
-
userspace: false,
|
|
82
|
-
sudo: false
|
|
83
|
-
});
|
|
84
|
-
var execCommandRaw = async (command, { store = {}, signal, env, cwd, prefix = "" } = {}, ctx) => {
|
|
85
|
-
const finalCommand = ctx?.sudo ? `sudo ${command}` : command;
|
|
86
|
-
const spawnResult = spawn(["bash", "-c", finalCommand], {
|
|
87
|
-
stdin: "inherit",
|
|
88
|
-
stdout: "pipe",
|
|
89
|
-
stderr: "pipe",
|
|
90
|
-
signal,
|
|
91
|
-
cwd,
|
|
92
|
-
env
|
|
93
|
-
});
|
|
94
|
-
store.spawnResult = spawnResult;
|
|
95
|
-
const [stdout, stderr] = await Promise.all([
|
|
96
|
-
teeStdout(spawnResult.stdout, prefix),
|
|
97
|
-
teeStderr(spawnResult.stderr, prefix)
|
|
98
|
-
]);
|
|
99
|
-
await spawnResult.exited;
|
|
100
|
-
return {
|
|
101
|
-
stderr,
|
|
102
|
-
stdout,
|
|
103
|
-
spawnResult
|
|
104
|
-
};
|
|
105
|
-
};
|
|
106
|
-
var execCommandMayError = async (command, props, context) => {
|
|
107
|
-
printCommand(command);
|
|
108
|
-
return execCommandRaw(command, props, context);
|
|
109
|
-
};
|
|
110
|
-
var execCommand = async (command, props, context) => {
|
|
111
|
-
const result = await execCommandMayError(command, props, context);
|
|
112
|
-
if (result.spawnResult.exitCode !== 0) {
|
|
113
|
-
console.error(`Error exit code: ${result.spawnResult.exitCode}`);
|
|
114
|
-
console.error(`Command: ${command}`);
|
|
115
|
-
throw new Error(command);
|
|
116
|
-
}
|
|
117
|
-
return result;
|
|
118
|
-
};
|
|
119
|
-
var copyFilesBySsh = async (from, to, context) => {
|
|
120
|
-
await execCommand(`rsync -avz ${from} ${context.getAddress()}:${to}`);
|
|
121
|
-
};
|
|
122
|
-
var execCommandOverSsh = async (command, context) => {
|
|
123
|
-
const sshKeyPart = context.sshKey ? ` -i ${context.sshKey}` : "";
|
|
124
|
-
return await execCommand(`ssh${sshKeyPart} ${context.getAddress()} '${command}'`);
|
|
125
|
-
};
|
|
126
|
-
// src/init.ts
|
|
127
|
-
var installBunOverSsh = async (context) => {
|
|
128
|
-
const bunPath = new URL(import.meta.resolve("./bun.sh")).pathname;
|
|
129
|
-
await copyFilesBySsh(bunPath, "./bun.sh", context);
|
|
130
|
-
await execCommandOverSsh("./bun.sh", context);
|
|
131
|
-
};
|
|
132
|
-
// src/podman.ts
|
|
133
|
-
var {$: $2 } = globalThis.Bun;
|
|
134
|
-
|
|
135
|
-
// src/packages.ts
|
|
136
|
-
var {$ } = globalThis.Bun;
|
|
137
|
-
var dnfOs = ["rocky", "fedora", "alma"];
|
|
138
|
-
var aptOs = ["ubuntu"];
|
|
139
|
-
var getManagerByOs = (osName) => {
|
|
140
|
-
if (aptOs.includes(osName)) {
|
|
141
|
-
return "apt";
|
|
142
|
-
} else if (dnfOs.includes(osName)) {
|
|
143
|
-
return "dnf";
|
|
144
|
-
} else {
|
|
145
|
-
throw new Error(`Unsupported OS: ${osName}`);
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
var getManagerConfig = (manager, packageConfig) => {
|
|
149
|
-
if (packageConfig.managers && manager in packageConfig.managers) {
|
|
150
|
-
const managerEnt = packageConfig.managers[manager];
|
|
151
|
-
return managerEnt;
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
name: packageConfig.name,
|
|
155
|
-
command: packageConfig.command
|
|
156
|
-
};
|
|
157
|
-
};
|
|
158
|
-
var getInstallCommand = (manager, config) => {
|
|
159
|
-
if (manager === "apt") {
|
|
160
|
-
return `apt install -y ${config.name}`;
|
|
161
|
-
} else if (manager === "dnf") {
|
|
162
|
-
return `dnf install -y ${config.name}`;
|
|
163
|
-
} else {
|
|
164
|
-
throw new Error("Unknow manager");
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
var packages = {
|
|
168
|
-
nftables: {
|
|
169
|
-
name: "nftables",
|
|
170
|
-
command: "nft"
|
|
171
|
-
},
|
|
172
|
-
podman: {
|
|
173
|
-
name: "podman",
|
|
174
|
-
command: "podman"
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
var installSystemPackage = async (packageEnt, context) => {
|
|
178
|
-
const finalPackageEnt = typeof packageEnt === "string" ? packages[packageEnt] : packageEnt;
|
|
179
|
-
if (!finalPackageEnt) {
|
|
180
|
-
const errorMessage = `No package ent for: ${packageEnt}`;
|
|
181
|
-
throw new Error(errorMessage);
|
|
182
|
-
}
|
|
183
|
-
const osName = (await $`cat /etc/os-release | grep ^ID= | cut -d'=' -f2`.text()).trim().replace(/"/g, "");
|
|
184
|
-
const manager = getManagerByOs(osName);
|
|
185
|
-
const managerConfig = getManagerConfig(manager, finalPackageEnt);
|
|
186
|
-
const checkResult = await execCommandMayError(`command -v ${managerConfig.command}`, {}, context);
|
|
187
|
-
if (checkResult.spawnResult.exitCode === 0) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const installCommand = getInstallCommand(manager, managerConfig);
|
|
191
|
-
await execCommand(installCommand, {}, context);
|
|
192
|
-
if (osName === "ubuntu") {
|
|
193
|
-
await execCommand(`apt-get install -y ${managerConfig.name}`, {}, context);
|
|
194
|
-
} else if (dnfOs.includes(osName)) {
|
|
195
|
-
await execCommand(`dnf install -y ${managerConfig.name}`, {}, context);
|
|
196
|
-
} else {
|
|
197
|
-
throw new Error(`Unsupported OS: ${osName}`);
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
// src/files.ts
|
|
202
|
-
import { appendFile, exists } from "fs/promises";
|
|
203
|
-
var {file, write } = globalThis.Bun;
|
|
204
|
-
|
|
205
|
-
// src/path.ts
|
|
206
|
-
import { homedir } from "os";
|
|
207
|
-
import { join } from "path";
|
|
208
|
-
function normalizePath(rawFilePath) {
|
|
209
|
-
let finalFilePath = rawFilePath.trim();
|
|
210
|
-
if (finalFilePath.startsWith("~/")) {
|
|
211
|
-
finalFilePath = join(homedir(), finalFilePath.slice(2));
|
|
212
|
-
}
|
|
213
|
-
return finalFilePath;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// src/files.ts
|
|
217
|
-
var normalizeFileContent = (str) => {
|
|
218
|
-
if (str === "") {
|
|
219
|
-
return str;
|
|
220
|
-
}
|
|
221
|
-
if (str.at(-1) !== `
|
|
222
|
-
`) {
|
|
223
|
-
return str + `
|
|
224
|
-
`;
|
|
225
|
-
}
|
|
226
|
-
return str;
|
|
227
|
-
};
|
|
228
|
-
var createDir = async (str) => {
|
|
229
|
-
const dirname = str.split("/").slice(0, -1).join("/");
|
|
230
|
-
const exist = await exists(dirname);
|
|
231
|
-
if (exist) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
await execCommand(`mkdir -p ${dirname}`);
|
|
235
|
-
};
|
|
236
|
-
var checkStrInFile = async (filePath, str) => {
|
|
237
|
-
const file2 = Bun.file(filePath);
|
|
238
|
-
if (!await file2.exists()) {
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
const fileText = await file2.text();
|
|
242
|
-
if (fileText.includes(str)) {
|
|
243
|
-
return true;
|
|
244
|
-
}
|
|
245
|
-
return false;
|
|
246
|
-
};
|
|
247
|
-
var createFileIfNeed = async (rawFilePath) => {
|
|
248
|
-
const filePath = normalizePath(rawFilePath);
|
|
249
|
-
await createDir(filePath);
|
|
250
|
-
if (!await file(filePath).exists()) {
|
|
251
|
-
await execCommand(`touch ${filePath}`);
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
var writeIfNewStr = async (rawFilePath, str) => {
|
|
255
|
-
const filePath = normalizePath(rawFilePath);
|
|
256
|
-
await createFileIfNeed(filePath);
|
|
257
|
-
if (await checkStrInFile(filePath, str)) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
await appendFile(filePath, normalizeFileContent(str));
|
|
261
|
-
};
|
|
262
|
-
var writeFile = async (pathToFile, str) => {
|
|
263
|
-
const normalizedPath = normalizePath(pathToFile);
|
|
264
|
-
await createFileIfNeed(normalizedPath);
|
|
265
|
-
await write(normalizedPath, str);
|
|
266
|
-
};
|
|
267
|
-
var writeFileFull = async (pathToFile, str) => {
|
|
268
|
-
const normalizedPath = normalizePath(pathToFile);
|
|
269
|
-
await createFileIfNeed(normalizedPath);
|
|
270
|
-
const fileText = await file(normalizedPath).text();
|
|
271
|
-
if (fileText === str) {
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
await write(normalizedPath, str);
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
// src/systemd.ts
|
|
278
|
-
var systemctlWordLangth = "systemctl ".length;
|
|
279
|
-
var formatUserspace = (command, context = defaultContext) => {
|
|
280
|
-
const userPart = context.userspace ? " --user " : "";
|
|
281
|
-
return "systemctl" + userPart + " " + command.slice(systemctlWordLangth);
|
|
282
|
-
};
|
|
283
|
-
var reloadSystemd = async (context = defaultContext) => {
|
|
284
|
-
await execCommand(formatUserspace("systemctl daemon-reload", context));
|
|
285
|
-
};
|
|
286
|
-
var startSystemdUnit = async (unitName, context = defaultContext) => {
|
|
287
|
-
await execCommand(formatUserspace(`systemctl start ${unitName}`, context));
|
|
288
|
-
};
|
|
289
|
-
var enabledSystemdUnit = async (unitName, context = defaultContext) => {
|
|
290
|
-
await execCommand(formatUserspace(`systemctl enable ${unitName}`, context));
|
|
291
|
-
};
|
|
292
|
-
var restartSystemdUnit = async (name, context = defaultContext) => {
|
|
293
|
-
await execCommand(formatUserspace(`systemctl restart ${name}`, context));
|
|
294
|
-
};
|
|
295
|
-
var stopSystemdUnit = async (name, context = defaultContext) => {
|
|
296
|
-
await execCommand(formatUserspace(`systemctl stop ${name}`, context));
|
|
297
|
-
};
|
|
298
|
-
var checkSystemdUnit = async (serviceName, context = defaultContext) => {
|
|
299
|
-
const { spawnResult } = await execCommandMayError(formatUserspace(`systemctl is-active ${serviceName}`, context));
|
|
300
|
-
return spawnResult.exitCode === 0;
|
|
301
|
-
};
|
|
302
|
-
var createSystemdUnit = async (unitName, content, context = defaultContext) => {
|
|
303
|
-
const pathToSeviceTarget = getSystemdPathToUnit(unitName, context);
|
|
304
|
-
await writeFileFull(pathToSeviceTarget, content);
|
|
305
|
-
await reloadSystemd(context);
|
|
306
|
-
};
|
|
307
|
-
var getSystemdPathToUnit = (serviceName, context = defaultContext) => {
|
|
308
|
-
if (context.userspace) {
|
|
309
|
-
return `~/.config/systemd/user/${serviceName}`;
|
|
310
|
-
}
|
|
311
|
-
return `/etc/systemd/system/${serviceName}`;
|
|
312
|
-
};
|
|
313
|
-
var createSystemdService = async (serviceName, content, context = defaultContext) => {
|
|
314
|
-
await createSystemdUnit(serviceName, content, context);
|
|
315
|
-
await enabledSystemdUnit(serviceName, context);
|
|
316
|
-
await startSystemdUnit(serviceName, context);
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
// src/nft.ts
|
|
320
|
-
var createNftTable = ({
|
|
321
|
-
allowedIpV4,
|
|
322
|
-
allowedPorts = []
|
|
323
|
-
}) => {
|
|
324
|
-
const nftTable = `table inet ramm {
|
|
325
|
-
set allowed_ipv4 {
|
|
326
|
-
type ipv4_addr
|
|
327
|
-
flags dynamic
|
|
328
|
-
elements = { ${allowedIpV4.join(`
|
|
329
|
-
`)} }
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
chain local_chain_base {
|
|
333
|
-
iif "lo" accept
|
|
334
|
-
ct state established,related accept
|
|
335
|
-
ip saddr @allowed_ipv4 tcp dport 22 accept
|
|
336
|
-
tcp dport 22 ct state new limit rate over 700/minute burst 5 packets drop
|
|
337
|
-
tcp dport 22 accept
|
|
338
|
-
${allowedPorts.map((port) => {
|
|
339
|
-
return `tcp dport ${port} accept`;
|
|
340
|
-
}).join(`
|
|
341
|
-
`)}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
chain local_chain {
|
|
345
|
-
jump local_chain_base
|
|
346
|
-
drop
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
chain prerouting {
|
|
350
|
-
type filter hook prerouting priority mangle; policy accept;
|
|
351
|
-
fib daddr type local jump local_chain
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
`;
|
|
355
|
-
return nftTable;
|
|
356
|
-
};
|
|
357
|
-
var setupNftable = async ({
|
|
358
|
-
allowedIpV4,
|
|
359
|
-
allowedPorts
|
|
360
|
-
}) => {
|
|
361
|
-
await installSystemPackage("nftables");
|
|
362
|
-
const listTable = await execCommandMayError("nft list table inet ramm");
|
|
363
|
-
if (listTable.spawnResult.exitCode === 0) {
|
|
364
|
-
await execCommand("nft delete table inet ramm");
|
|
365
|
-
}
|
|
366
|
-
const nftTable = createNftTable({ allowedIpV4, allowedPorts });
|
|
367
|
-
await execCommand(`nft -f - <<EOF
|
|
368
|
-
${nftTable}
|
|
369
|
-
EOF`);
|
|
370
|
-
await safeNftTable();
|
|
371
|
-
};
|
|
372
|
-
var safeNftTable = async () => {
|
|
373
|
-
await execCommand("nft list ruleset > /etc/nftables.conf");
|
|
374
|
-
await execCommand("systemctl enable nftables");
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
// src/podman.ts
|
|
378
|
-
var installPodman = async () => {
|
|
379
|
-
if ((await $2`command -v podman`.nothrow().quiet()).exitCode !== 0) {
|
|
380
|
-
await installSystemPackage("podman");
|
|
381
|
-
}
|
|
382
|
-
await createNetwork();
|
|
383
|
-
};
|
|
384
|
-
var createNetwork = async () => {
|
|
385
|
-
const netwroks = await execCommand("podman network inspect $(podman network ls -q) -f '{{.NetworkInterface}}'");
|
|
386
|
-
const podmanNetworks = await execCommand("podman network ls");
|
|
387
|
-
if (podmanNetworks.stdout.includes("ramm")) {
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
if (netwroks.stdout.includes("podman_ramm")) {
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
await execCommand("podman network create --interface-name=podman_ramm ramm");
|
|
394
|
-
};
|
|
395
|
-
var getCreateCommand = async (name) => {
|
|
396
|
-
const podmanCreateCommand = await $2`podman inspect --format '{{.Config.CreateCommand}}' ${name}`.nothrow().quiet();
|
|
397
|
-
return podmanCreateCommand.text().slice(0, -1).slice(1, -1) || "";
|
|
398
|
-
};
|
|
399
|
-
var loginPodman = async (address, login, password) => {
|
|
400
|
-
return await execCommand(`echo "${password}" | podman login --username "${login}" --password-stdin ${address}`);
|
|
401
|
-
};
|
|
402
|
-
var createPodmanCommand = ({
|
|
403
|
-
name,
|
|
404
|
-
replace = true,
|
|
405
|
-
background = true,
|
|
406
|
-
networks = ["ramm"],
|
|
407
|
-
envs = [],
|
|
408
|
-
volumes = [],
|
|
409
|
-
command
|
|
410
|
-
}) => {
|
|
411
|
-
const values = [];
|
|
412
|
-
values.push("podman", "run");
|
|
413
|
-
if (name) {
|
|
414
|
-
values.push(`--name ${name}`);
|
|
415
|
-
}
|
|
416
|
-
if (replace) {
|
|
417
|
-
values.push("--replace");
|
|
418
|
-
}
|
|
419
|
-
if (background) {
|
|
420
|
-
values.push("-d");
|
|
421
|
-
}
|
|
422
|
-
for (const network of networks) {
|
|
423
|
-
values.push(`--network ${network}`);
|
|
424
|
-
}
|
|
425
|
-
for (const env of envs) {
|
|
426
|
-
values.push(`-e ${env.name}=${env.value}`);
|
|
427
|
-
}
|
|
428
|
-
for (const volume of volumes) {
|
|
429
|
-
values.push(`-v ${volume.from}:${volume.to}`);
|
|
430
|
-
}
|
|
431
|
-
values.push(command);
|
|
432
|
-
const str = values.join(" ");
|
|
433
|
-
return str;
|
|
434
|
-
};
|
|
435
|
-
var runPodmanContainer = async (name, command) => {
|
|
436
|
-
if (await getCreateCommand(name) !== command) {
|
|
437
|
-
await $2`podman rm -f ${name}`;
|
|
438
|
-
await execCommand(command);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
console.info("Podman container is already running");
|
|
442
|
-
};
|
|
443
|
-
var runPodmanContainerService = async (name, command, context = defaultContext) => {
|
|
444
|
-
const serviceName = `${name}.service`;
|
|
445
|
-
const filepath = getSystemdPathToUnit(serviceName);
|
|
446
|
-
if (await checkSystemdUnit(serviceName, context)) {
|
|
447
|
-
await stopSystemdUnit(serviceName, context);
|
|
448
|
-
}
|
|
449
|
-
await runPodmanContainer(name, command);
|
|
450
|
-
await execCommand(`podman generate systemd --name --new ${name} > ${filepath}`);
|
|
451
|
-
await reloadSystemd(context);
|
|
452
|
-
await startSystemdUnit(serviceName, context);
|
|
453
|
-
await enabledSystemdUnit(serviceName, context);
|
|
454
|
-
};
|
|
455
|
-
var addNftPodmanRule = async () => {
|
|
456
|
-
const podmanNetworksResult = await execCommand("podman network inspect $(podman network ls -q) -f '{{.NetworkInterface}}'");
|
|
457
|
-
const podmanNetworks = podmanNetworksResult.stdout.trim().split(`
|
|
458
|
-
`);
|
|
459
|
-
await execCommand(`nft add set inet ramm podman_interfaces '{ type ifname; flags dynamic; elements = { ${podmanNetworks.join(", ")} }; }'`);
|
|
460
|
-
await execCommand("nft insert rule inet ramm prerouting iifname @podman_interfaces accept");
|
|
461
|
-
await safeNftTable();
|
|
462
|
-
};
|
|
463
|
-
// src/ssh.ts
|
|
464
|
-
var {file: file2 } = globalThis.Bun;
|
|
465
|
-
var addKeyToHostConfig = async (pathToHost, address, pathToKey) => {
|
|
466
|
-
const text = `Host ${address}
|
|
467
|
-
IdentityFile ${pathToKey}
|
|
468
|
-
`;
|
|
469
|
-
await writeIfNewStr(pathToHost, text);
|
|
470
|
-
};
|
|
471
|
-
var getServerFingerprint = async (context) => {
|
|
472
|
-
const { stdout } = await execCommandOverSsh('ssh-keyscan -t ed25519 localhost | grep -v "^#"', context);
|
|
473
|
-
return stdout.replace("localhost", context.domain);
|
|
474
|
-
};
|
|
475
|
-
var saveSshFingerptint = async (filePath, context) => {
|
|
476
|
-
const normalizedPath = normalizePath(filePath);
|
|
477
|
-
const fingerprint = await getServerFingerprint(context);
|
|
478
|
-
await writeFileFull(filePath, normalizedPath);
|
|
479
|
-
};
|
|
480
|
-
async function createSshKey(filePath, comment) {
|
|
481
|
-
printFunction(`${createSshKey.name} ${filePath}`);
|
|
482
|
-
const normalizedPathToKey = normalizePath(filePath);
|
|
483
|
-
const name = normalizedPathToKey.split("/").at(-1);
|
|
484
|
-
const pathToKeyPub = `${normalizedPathToKey}.pub`;
|
|
485
|
-
const pathToDir = normalizedPathToKey.split("/").slice(0, -1).join("/");
|
|
486
|
-
const pathToKeyFile = file2(normalizedPathToKey);
|
|
487
|
-
const pathToKeyPubFile = file2(pathToKeyPub);
|
|
488
|
-
if (await pathToKeyFile.exists() && await pathToKeyPubFile.exists()) {
|
|
489
|
-
return await pathToKeyPubFile.text();
|
|
490
|
-
}
|
|
491
|
-
await execCommand(`mkdir -p ${pathToDir}`);
|
|
492
|
-
await execCommand(`ssh-keygen -t ed25519 -f ${normalizedPathToKey} -N "" -C "${comment || name}"`);
|
|
493
|
-
await execCommand(`chmod 600 ${normalizedPathToKey}`);
|
|
494
|
-
const pubKey = await Bun.file(pathToKeyPub).text();
|
|
495
|
-
return pubKey;
|
|
496
|
-
}
|
|
497
|
-
var addSshKeyToAuthorizedOverSsh = async (pubKey, context) => {
|
|
498
|
-
printFunction(`${addSshKeyToAuthorizedOverSsh.name} ${pubKey}`);
|
|
499
|
-
const { stdout: keys } = await execCommandOverSsh("cat .ssh/authorized_keys", context);
|
|
500
|
-
if (keys.includes(pubKey)) {
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
await execCommandOverSsh(`echo "${pubKey}" >> .ssh/authorized_keys`, context);
|
|
504
|
-
};
|
|
505
|
-
var createAndAddSshKey = async (filePath, comment, context) => {
|
|
506
|
-
const normalizedPath = normalizePath(filePath);
|
|
507
|
-
const pubKey = await createSshKey(normalizedPath, comment);
|
|
508
|
-
console.log(`Key is: ${pubKey}`);
|
|
509
|
-
await addSshKeyToAuthorizedOverSsh(pubKey, context);
|
|
510
|
-
};
|
|
511
|
-
var addSshKeyToUse = async ({
|
|
512
|
-
key,
|
|
513
|
-
fingerprint,
|
|
514
|
-
filePath,
|
|
515
|
-
server
|
|
516
|
-
}) => {
|
|
517
|
-
printFunction(`${addSshKeyToUse.name} ${filePath}`);
|
|
518
|
-
const normalizedFilePath = normalizePath(filePath);
|
|
519
|
-
await writeFileFull(normalizedFilePath, key);
|
|
520
|
-
await execCommand(`chmod 0600 ${normalizedFilePath}`);
|
|
521
|
-
await writeIfNewStr("~/.ssh/known_hosts", fingerprint);
|
|
522
|
-
await addKeyToHostConfig("~/.ssh/config", server, normalizedFilePath);
|
|
523
|
-
};
|
|
524
|
-
// src/cron.ts
|
|
525
|
-
var createCron = async ({
|
|
526
|
-
time,
|
|
527
|
-
pathToFile
|
|
528
|
-
}) => {
|
|
529
|
-
const pathToFileNorm = normalizePath(pathToFile);
|
|
530
|
-
const constructedLine = `${time} ${pathToFileNorm}`;
|
|
531
|
-
const tempFile = `/tmp/ramm_cron}`;
|
|
532
|
-
const { stdout: cronConfig } = await execCommandMayError("crontab -l");
|
|
533
|
-
let newCronConfig = cronConfig;
|
|
534
|
-
if (cronConfig.includes(constructedLine)) {
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
if (cronConfig.includes(pathToFileNorm)) {
|
|
538
|
-
newCronConfig = cronConfig.split(`
|
|
539
|
-
`).filter((str) => !str.includes(pathToFileNorm)).join(`
|
|
540
|
-
`);
|
|
541
|
-
}
|
|
542
|
-
newCronConfig = normalizeFileContent(normalizeFileContent(cronConfig) + constructedLine);
|
|
543
|
-
await writeFile(tempFile, newCronConfig);
|
|
544
|
-
await execCommandMayError(`cat ${tempFile}`);
|
|
545
|
-
await execCommandMayError(`crontab ${tempFile}`);
|
|
546
|
-
await execCommandMayError(`rm ${tempFile}`);
|
|
547
|
-
};
|
|
548
|
-
// src/build.ts
|
|
549
|
-
var {build, file: file3 } = globalThis.Bun;
|
|
550
|
-
var buildAndRunOverSsh = async ({
|
|
551
|
-
entrypoint,
|
|
552
|
-
context
|
|
553
|
-
}) => {
|
|
554
|
-
const normalizedEntrypoint = normalizePath(entrypoint);
|
|
555
|
-
const distName = `ramm_dist`;
|
|
556
|
-
const distDir = `/tmp/${distName}`;
|
|
557
|
-
const outputs = await build({
|
|
558
|
-
outdir: distDir,
|
|
559
|
-
entrypoints: [normalizedEntrypoint],
|
|
560
|
-
target: "bun"
|
|
561
|
-
});
|
|
562
|
-
const pathToDistFile = outputs.outputs[0]?.path;
|
|
563
|
-
const relativePathToFile = pathToDistFile.slice(distDir.length + 1);
|
|
564
|
-
await copyFilesBySsh(`${distDir}/`, distDir, context);
|
|
565
|
-
await execCommandOverSsh(`bun run ${distDir}/${relativePathToFile}`, context);
|
|
566
|
-
await execCommand(`rm -rf ${distDir}`);
|
|
567
|
-
};
|
|
568
|
-
var pathToJson = "/tmp/ramm_json";
|
|
569
|
-
var passVarsClient = async (data, context) => {
|
|
570
|
-
printFunction("passVarsClient");
|
|
571
|
-
const json = JSON.stringify(data);
|
|
572
|
-
await writeFile(pathToJson, json);
|
|
573
|
-
await copyFilesBySsh(pathToJson, pathToJson, context);
|
|
574
|
-
await execCommand(`rm -rf ${pathToJson}`);
|
|
575
|
-
};
|
|
576
|
-
var passVarsServer = async () => {
|
|
577
|
-
printFunction("passVarsServer");
|
|
578
|
-
const jsonData = await file3(pathToJson).json();
|
|
579
|
-
await execCommand(`rm -rf ${pathToJson}`);
|
|
580
|
-
return jsonData;
|
|
581
|
-
};
|
|
582
|
-
export {
|
|
583
|
-
writeIfNewStr,
|
|
584
|
-
writeFileFull,
|
|
585
|
-
writeFile,
|
|
586
|
-
startSystemdUnit,
|
|
587
|
-
setupNftable,
|
|
588
|
-
saveSshFingerptint,
|
|
589
|
-
runPodmanContainerService,
|
|
590
|
-
runPodmanContainer,
|
|
591
|
-
restartSystemdUnit,
|
|
592
|
-
reloadSystemd,
|
|
593
|
-
printBlock,
|
|
594
|
-
passVarsServer,
|
|
595
|
-
passVarsClient,
|
|
596
|
-
normalizePath,
|
|
597
|
-
normalizeFileContent,
|
|
598
|
-
loginPodman,
|
|
599
|
-
installSystemPackage,
|
|
600
|
-
installPodman,
|
|
601
|
-
installBunOverSsh,
|
|
602
|
-
getSystemdPathToUnit as getSystemdPathToService,
|
|
603
|
-
getServerFingerprint,
|
|
604
|
-
execCommandRaw,
|
|
605
|
-
execCommandOverSsh,
|
|
606
|
-
execCommandMayError,
|
|
607
|
-
execCommand,
|
|
608
|
-
enabledSystemdUnit,
|
|
609
|
-
createSystemdUnit,
|
|
610
|
-
createSystemdService,
|
|
611
|
-
createPodmanCommand,
|
|
612
|
-
createCron,
|
|
613
|
-
createAndAddSshKey,
|
|
614
|
-
copyFilesBySsh,
|
|
615
|
-
buildAndRunOverSsh,
|
|
616
|
-
addSshKeyToUse,
|
|
617
|
-
addNftPodmanRule,
|
|
618
|
-
addKeyToHostConfig,
|
|
619
|
-
Context
|
|
620
|
-
};
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { type Subprocess } from "bun";
|
|
2
|
-
import { Context } from "../context.ts";
|
|
3
|
-
export declare const defaultContext: Context;
|
|
4
|
-
export type ExecCommandStore = {
|
|
5
|
-
spawnResult?: Subprocess<"inherit", "pipe", "pipe">;
|
|
6
|
-
};
|
|
7
|
-
type ExecProps = {
|
|
8
|
-
store?: ExecCommandStore;
|
|
9
|
-
env?: Record<string, string>;
|
|
10
|
-
cwd?: string;
|
|
11
|
-
prefix?: string;
|
|
12
|
-
signal?: AbortSignal;
|
|
13
|
-
} | void;
|
|
14
|
-
export declare const execCommandRaw: (command: string, { store, signal, env, cwd, prefix }?: ExecProps, ctx?: Context) => Promise<{
|
|
15
|
-
stderr: string;
|
|
16
|
-
stdout: string;
|
|
17
|
-
spawnResult: Subprocess<"inherit", "pipe", "pipe">;
|
|
18
|
-
}>;
|
|
19
|
-
export declare const execCommandMayError: (command: string, props: ExecProps, context?: Context) => Promise<{
|
|
20
|
-
stderr: string;
|
|
21
|
-
stdout: string;
|
|
22
|
-
spawnResult: Subprocess<"inherit", "pipe", "pipe">;
|
|
23
|
-
}>;
|
|
24
|
-
export declare const execCommand: (command: string, props: ExecProps, context?: Context) => Promise<{
|
|
25
|
-
stderr: string;
|
|
26
|
-
stdout: string;
|
|
27
|
-
spawnResult: Subprocess<"inherit", "pipe", "pipe">;
|
|
28
|
-
}>;
|
|
29
|
-
export declare const copyFilesBySsh: (from: string, to: string, context: Context) => Promise<void>;
|
|
30
|
-
export declare const execCommandOverSsh: (command: string, context: Context) => Promise<{
|
|
31
|
-
stderr: string;
|
|
32
|
-
stdout: string;
|
|
33
|
-
spawnResult: Subprocess<"inherit", "pipe", "pipe">;
|
|
34
|
-
}>;
|
|
35
|
-
export {};
|
package/dist/types/base/tee.d.ts
DELETED
package/dist/types/build.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Context } from "./context.ts";
|
|
2
|
-
export declare const buildAndRunOverSsh: ({ entrypoint, context, }: {
|
|
3
|
-
entrypoint: string;
|
|
4
|
-
context: Context;
|
|
5
|
-
}) => Promise<void>;
|
|
6
|
-
export declare const passVarsClient: (data: Record<string, any>, context: Context) => Promise<void>;
|
|
7
|
-
export declare const passVarsServer: () => Promise<any>;
|
package/dist/types/context.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export declare class Context {
|
|
2
|
-
user: string;
|
|
3
|
-
domain: string;
|
|
4
|
-
userspace: boolean;
|
|
5
|
-
sudo: boolean;
|
|
6
|
-
sshKey?: string;
|
|
7
|
-
params: Record<string, string>;
|
|
8
|
-
constructor({ user, address, userspace, sudo, sshKey, }: {
|
|
9
|
-
user: string;
|
|
10
|
-
address: string;
|
|
11
|
-
userspace?: boolean;
|
|
12
|
-
sudo?: boolean;
|
|
13
|
-
sshKey?: string;
|
|
14
|
-
});
|
|
15
|
-
getAddress(): string;
|
|
16
|
-
}
|
package/dist/types/cron.d.ts
DELETED
package/dist/types/files.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export declare const normalizeFileContent: (str: string) => string;
|
|
2
|
-
export declare const writeIfNewStr: (rawFilePath: string, str: string) => Promise<void>;
|
|
3
|
-
export declare const writeFile: (pathToFile: string, str: string) => Promise<void>;
|
|
4
|
-
export declare const writeFileFull: (pathToFile: string, str: string) => Promise<void>;
|
package/dist/types/init.d.ts
DELETED
package/dist/types/nft.d.ts
DELETED
package/dist/types/packages.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Context } from "./context.ts";
|
|
2
|
-
type Manager = "dnf" | "apt";
|
|
3
|
-
type PackageConfig = {
|
|
4
|
-
name: string;
|
|
5
|
-
command: string;
|
|
6
|
-
managers?: Record<Manager, ManagerConfig>;
|
|
7
|
-
};
|
|
8
|
-
type ManagerConfig = {
|
|
9
|
-
name: string;
|
|
10
|
-
command: string;
|
|
11
|
-
};
|
|
12
|
-
export declare const installSystemPackage: (packageEnt: string | PackageConfig, context?: Context) => Promise<void>;
|
|
13
|
-
export {};
|
package/dist/types/path.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function normalizePath(rawFilePath: string): string;
|
package/dist/types/podman.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { Context } from "./context.ts";
|
|
2
|
-
export declare const installPodman: () => Promise<void>;
|
|
3
|
-
export declare const loginPodman: (address: string, login: string, password: string) => Promise<{
|
|
4
|
-
stderr: string;
|
|
5
|
-
stdout: string;
|
|
6
|
-
spawnResult: import("bun").Subprocess<"inherit", "pipe", "pipe">;
|
|
7
|
-
}>;
|
|
8
|
-
export declare const createPodmanCommand: ({ name, replace, background, networks, envs, volumes, command, }: {
|
|
9
|
-
name?: string;
|
|
10
|
-
replace?: boolean;
|
|
11
|
-
background?: boolean;
|
|
12
|
-
networks?: string[];
|
|
13
|
-
envs?: {
|
|
14
|
-
name: string;
|
|
15
|
-
value: string;
|
|
16
|
-
}[];
|
|
17
|
-
volumes?: {
|
|
18
|
-
from: string;
|
|
19
|
-
to: string;
|
|
20
|
-
}[];
|
|
21
|
-
command: string;
|
|
22
|
-
}) => string;
|
|
23
|
-
export declare const runPodmanContainer: (name: string, command: string) => Promise<void>;
|
|
24
|
-
export declare const runPodmanContainerService: (name: string, command: string, context?: Context) => Promise<void>;
|
|
25
|
-
export declare const addNftPodmanRule: () => Promise<void>;
|
package/dist/types/print.d.ts
DELETED
package/dist/types/ramm.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export { execCommandOverSsh, execCommand, execCommandMayError, copyFilesBySsh, execCommandRaw, } from "./base/base.ts";
|
|
2
|
-
export { Context } from "./context.ts";
|
|
3
|
-
export { installBunOverSsh } from "./init.ts";
|
|
4
|
-
export { installPodman, runPodmanContainer, loginPodman, runPodmanContainerService, addNftPodmanRule, createPodmanCommand, } from "./podman.ts";
|
|
5
|
-
export { startSystemdUnit, enabledSystemdUnit, restartSystemdUnit, createSystemdService, getSystemdPathToUnit as getSystemdPathToService, createSystemdUnit, reloadSystemd, } from "./systemd.ts";
|
|
6
|
-
export { installSystemPackage } from "./packages.ts";
|
|
7
|
-
export { printBlock } from "./print.ts";
|
|
8
|
-
export { setupNftable } from "./nft.ts";
|
|
9
|
-
export { writeIfNewStr, writeFile, writeFileFull, normalizeFileContent, } from "./files.ts";
|
|
10
|
-
export { createAndAddSshKey, getServerFingerprint, addKeyToHostConfig, addSshKeyToUse, saveSshFingerptint, } from "./ssh.ts";
|
|
11
|
-
export { normalizePath } from "./path.ts";
|
|
12
|
-
export { createCron } from "./cron.ts";
|
|
13
|
-
export { buildAndRunOverSsh, passVarsClient, passVarsServer } from "./build.ts";
|
package/dist/types/ssh.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Context } from "./context.ts";
|
|
2
|
-
export declare const addKeyToHostConfig: (pathToHost: string, address: string, pathToKey: string) => Promise<void>;
|
|
3
|
-
export declare const getServerFingerprint: (context: Context) => Promise<string>;
|
|
4
|
-
export declare const saveSshFingerptint: (filePath: string, context: Context) => Promise<void>;
|
|
5
|
-
export declare const createAndAddSshKey: (filePath: string, comment: string, context: Context) => Promise<void>;
|
|
6
|
-
export declare const addSshKeyToUse: ({ key, fingerprint, filePath, server, }: {
|
|
7
|
-
key: string;
|
|
8
|
-
fingerprint: string;
|
|
9
|
-
filePath: string;
|
|
10
|
-
server: string;
|
|
11
|
-
}) => Promise<void>;
|
package/dist/types/systemd.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { Context } from "./context.ts";
|
|
2
|
-
export declare const reloadSystemd: (context?: Context) => Promise<void>;
|
|
3
|
-
export declare const startSystemdUnit: (unitName: string, context?: Context) => Promise<void>;
|
|
4
|
-
export declare const enabledSystemdUnit: (unitName: string, context?: Context) => Promise<void>;
|
|
5
|
-
export declare const restartSystemdUnit: (name: string, context?: Context) => Promise<void>;
|
|
6
|
-
export declare const stopSystemdUnit: (name: string, context?: Context) => Promise<void>;
|
|
7
|
-
export declare const checkSystemdUnit: (serviceName: string, context?: Context) => Promise<boolean>;
|
|
8
|
-
export declare const createSystemdUnit: (unitName: string, content: string, context?: Context) => Promise<void>;
|
|
9
|
-
export declare const getSystemdPathToUnit: (serviceName: string, context?: Context) => string;
|
|
10
|
-
export declare const createSystemdService: (serviceName: string, content: string, context?: Context) => Promise<void>;
|