paratix 0.12.8 → 0.14.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 +18 -10
- package/dist/{chunk-M3ZQJNKM.js → chunk-475OT32R.js} +260 -29
- package/dist/cli.js +2297 -1529
- package/dist/{index-udpAybq3.d.ts → index-DlZISXJx.d.ts} +27 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.js +46 -14
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +1 -2
- package/llm-guide.md +55 -45
- package/package.json +4 -2
- package/dist/chunk-M3ZQJNKM.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/modules/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -119,29 +119,37 @@ Paratix can also manage file-backed swap directly. Use `swap.file(...)` to provi
|
|
|
119
119
|
|
|
120
120
|
## CLI
|
|
121
121
|
|
|
122
|
+
`paratix --help` lists these options directly, so they are discoverable without drilling into `paratix apply --help` first.
|
|
123
|
+
|
|
122
124
|
```text
|
|
123
125
|
paratix apply <file> [options]
|
|
124
126
|
|
|
125
127
|
Options:
|
|
126
|
-
--diff
|
|
127
|
-
--dry-run
|
|
128
|
-
--env <key=value
|
|
129
|
-
--env-file <path>
|
|
130
|
-
--
|
|
131
|
-
--
|
|
132
|
-
--
|
|
133
|
-
--
|
|
134
|
-
--help
|
|
128
|
+
--diff Combined with --dry-run: unified diff per changed module
|
|
129
|
+
--dry-run Only check, do not apply (some runtime restarts stay unverified)
|
|
130
|
+
--env <key=value...> Set environment values for the playbook (repeatable)
|
|
131
|
+
--env-file <path> Load environment values from a dotenv file
|
|
132
|
+
--filter <names> Run only the named recipes/modules (comma-separated, repeatable)
|
|
133
|
+
--first-run Set PARATIX_FIRST_RUN=true before loading the playbook
|
|
134
|
+
--reconnect-timeout <seconds> SSH reconnect timeout for reboots/port changes (max 86400)
|
|
135
|
+
--verbose Show full stack traces on error
|
|
136
|
+
--help Show help
|
|
135
137
|
```
|
|
136
138
|
|
|
139
|
+
`--filter <names>` restricts the run to the named recipes and modules. Names are matched anywhere in the tree, and every node that is not selected is shown as `skipped` instead of being executed. The option is comma-separated and repeatable, so `--filter rybbit,palamedes-examples` and `--filter rybbit --filter palamedes-examples` are equivalent. Selecting a recipe runs its whole subtree; a recipe that is not selected but contains a selected descendant is still descended into, so only the matching children run while its siblings are skipped. If several nodes share the same name, every one of them is selected. An unknown filter name aborts the run before it connects (exit code 2). The final run summary counts only top-level nodes, so nested skipped nodes are still shown but are not added to the `skipped` tally. `--filter` composes with `--dry-run` and the other flags.
|
|
140
|
+
|
|
137
141
|
`--first-run` is meant for explicit bootstrap flows where a fresh server must be hardened first and the rest of the system should only be applied later.
|
|
138
142
|
|
|
139
143
|
`--diff` only works together with `--dry-run`. When enabled, modules that opt in print a unified diff below their status line, showing exactly which lines or values would change. Without `--diff`, the dry-run output is unchanged. Diff-producing modules: `file.copy`, `file.template`, `sysctl.set`, `hostname.set`, `swap.file`, `swap.swappiness`, `swap.vfsCachePressure`, `cron.job`, `cron.absent`, `timer.scheduled`, `timer.absent`, `net.hosts`, `quadlet.container`.
|
|
140
144
|
|
|
145
|
+
`--reconnect-timeout <seconds>` sets how long Paratix keeps retrying the SSH connection after a module forces the connection to drop — after a `system.reboot` or after an SSH port change. When a module reboots the host, Paratix waits a short grace period and then reconnects within a **300 second window by default**, so hosts that need a few minutes to come back (fsck, cloud-init, slow POST) still succeed. The number of attempts follows the time window rather than a fixed cap, so the window is never cut short. Port-change reconnects use a shorter 120 second default because the port comes back almost immediately. Set `--reconnect-timeout` (up to 86400 seconds) to override both paths for exceptionally slow reboots.
|
|
146
|
+
|
|
141
147
|
## Documentation
|
|
142
148
|
|
|
143
149
|
- For project scaffolding, see [`create-paratix`](https://www.npmjs.com/package/create-paratix).
|
|
144
|
-
-
|
|
150
|
+
- See the complete [TypeScript API and Module Reference](./llm-guide.md).
|
|
151
|
+
- For common runtime and connection failures, see [Troubleshooting](../../docs/user-guide/troubleshooting.md).
|
|
152
|
+
- Before upgrading an existing project, see the [migration notes](../../docs/user-guide/migration.md).
|
|
145
153
|
|
|
146
154
|
## License
|
|
147
155
|
|
|
@@ -1568,13 +1568,6 @@ function buildRepositoryUpdateFlag(name, expectedContent) {
|
|
|
1568
1568
|
flagPrefix
|
|
1569
1569
|
};
|
|
1570
1570
|
}
|
|
1571
|
-
function firstNonEmptyApt(text) {
|
|
1572
|
-
for (const line of text.split("\n")) {
|
|
1573
|
-
const trimmed = line.trim();
|
|
1574
|
-
if (trimmed.length > 0) return trimmed;
|
|
1575
|
-
}
|
|
1576
|
-
return null;
|
|
1577
|
-
}
|
|
1578
1571
|
async function probeAptRepositoryInodeIdentity(ssh2, filePath) {
|
|
1579
1572
|
const result = await ssh2.exec(`stat -c '%d:%i' ${shellQuote(filePath)}`, {
|
|
1580
1573
|
ignoreExitCode: true,
|
|
@@ -1615,7 +1608,7 @@ async function rollbackRepositoryAfterUpdateFailure(parameters) {
|
|
|
1615
1608
|
const failureMessage = `[apt.repository] apt-get update failed for ${name}`;
|
|
1616
1609
|
if (rollbackUpdate.code !== 0) {
|
|
1617
1610
|
return failedCommand(
|
|
1618
|
-
`${failureMessage}; rollback succeeded but apt-get update on the restored sources also failed (exit code ${String(rollbackUpdate.code)}): ${
|
|
1611
|
+
`${failureMessage}; rollback succeeded but apt-get update on the restored sources also failed (exit code ${String(rollbackUpdate.code)}): ${firstNonEmptyLine(rollbackUpdate.stderr) ?? firstNonEmptyLine(rollbackUpdate.stdout) ?? "no output"}`,
|
|
1619
1612
|
updateResult
|
|
1620
1613
|
);
|
|
1621
1614
|
}
|
|
@@ -3252,6 +3245,15 @@ async function commandCheckPassed(ssh2, check, secrets) {
|
|
|
3252
3245
|
});
|
|
3253
3246
|
return result.code === 0;
|
|
3254
3247
|
}
|
|
3248
|
+
function commandExecOptions(secrets, options) {
|
|
3249
|
+
const execOptions2 = {
|
|
3250
|
+
ignoreExitCode: true,
|
|
3251
|
+
secrets,
|
|
3252
|
+
silent: true
|
|
3253
|
+
};
|
|
3254
|
+
if (options?.timeout !== void 0) execOptions2.timeout = options.timeout;
|
|
3255
|
+
return execOptions2;
|
|
3256
|
+
}
|
|
3255
3257
|
var command = {
|
|
3256
3258
|
/**
|
|
3257
3259
|
* Run an arbitrary shell command on the remote host.
|
|
@@ -3271,6 +3273,7 @@ var command = {
|
|
|
3271
3273
|
* If it exits `0`, the command is considered already done.
|
|
3272
3274
|
* @param options.name - An optional display name shown in the run output.
|
|
3273
3275
|
* @param options.secrets - Secret values that must be redacted from command output.
|
|
3276
|
+
* @param options.timeout - Maximum runtime for the command in milliseconds.
|
|
3274
3277
|
* @returns A Module that executes the shell command.
|
|
3275
3278
|
*
|
|
3276
3279
|
* @example
|
|
@@ -3292,11 +3295,7 @@ var command = {
|
|
|
3292
3295
|
if (options?.check != null && await commandCheckPassed(ssh2, options.check, secrets)) {
|
|
3293
3296
|
return { status: "ok" };
|
|
3294
3297
|
}
|
|
3295
|
-
const result = await ssh2.exec(cmd,
|
|
3296
|
-
ignoreExitCode: true,
|
|
3297
|
-
secrets,
|
|
3298
|
-
silent: true
|
|
3299
|
-
});
|
|
3298
|
+
const result = await ssh2.exec(cmd, commandExecOptions(secrets, options));
|
|
3300
3299
|
if (result.code !== 0) {
|
|
3301
3300
|
return failedCommand(`[${moduleName}] command failed`, {
|
|
3302
3301
|
...result,
|
|
@@ -3323,10 +3322,12 @@ var EXEC_OPTS4 = { ignoreExitCode: true, silent: true };
|
|
|
3323
3322
|
var UNIT_NAME_PATTERN = new RegExp("^[\\w@.\\-]+$", "v");
|
|
3324
3323
|
var COMPOSE_CONFIG_MODE = "0600";
|
|
3325
3324
|
var COMPOSE_CONFIG_STAGING_PREFIX = ".compose.yml.paratix-staging";
|
|
3325
|
+
var COMPOSE_IMAGE_INSPECT_FORMAT = "{{.Id}}\\n{{range .RepoDigests}}{{.}}\\n{{end}}";
|
|
3326
3326
|
var SYSTEMD_UNIT_MODE = "0644";
|
|
3327
3327
|
var SYSTEMD_UNIT_STAGING_PREFIX = ".compose-systemd-unit.paratix-staging";
|
|
3328
3328
|
var C0_CONTROL_MAX_CODE_POINT = 31;
|
|
3329
3329
|
var DELETE_CONTROL_CODE_POINT2 = 127;
|
|
3330
|
+
var COMPOSE_CONFIG_JSON_MAX_BYTES = 1048576;
|
|
3330
3331
|
function requireComposeSsh(ssh2, action, projectDirectory) {
|
|
3331
3332
|
return ssh2 ?? failed(`[compose.${action}] SSH connection is required for ${projectDirectory}`);
|
|
3332
3333
|
}
|
|
@@ -3351,10 +3352,6 @@ var COMPOSE_UP_ACTION_KEYWORDS_REGEX = new RegExp("^(?:Creating|Recreating|Start
|
|
|
3351
3352
|
function composeUpReportedChange(composeOutput) {
|
|
3352
3353
|
return COMPOSE_UP_ACTION_KEYWORDS_REGEX.test(composeOutput);
|
|
3353
3354
|
}
|
|
3354
|
-
var COMPOSE_PULL_ACTION_REGEX = new RegExp("^(?:Pulling|Downloaded)\\s", "mv");
|
|
3355
|
-
function composePullReportedChange(composeOutput) {
|
|
3356
|
-
return COMPOSE_PULL_ACTION_REGEX.test(composeOutput);
|
|
3357
|
-
}
|
|
3358
3355
|
function validateComposeUpServices(services) {
|
|
3359
3356
|
for (const service2 of services ?? []) {
|
|
3360
3357
|
if (service2 === "") {
|
|
@@ -3392,6 +3389,148 @@ function parseComposeProjectName(stdout, projectDirectory) {
|
|
|
3392
3389
|
return null;
|
|
3393
3390
|
}
|
|
3394
3391
|
}
|
|
3392
|
+
function isRecord(value) {
|
|
3393
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3394
|
+
}
|
|
3395
|
+
function readComposeServiceImage(service2) {
|
|
3396
|
+
if (!isRecord(service2)) return null;
|
|
3397
|
+
const { image } = service2;
|
|
3398
|
+
return typeof image === "string" && image.trim() !== "" ? image : null;
|
|
3399
|
+
}
|
|
3400
|
+
function collectComposeConfigImages(services) {
|
|
3401
|
+
const images = /* @__PURE__ */ new Set();
|
|
3402
|
+
for (const service2 of Object.values(services)) {
|
|
3403
|
+
const image = readComposeServiceImage(service2);
|
|
3404
|
+
if (image !== null) images.add(image);
|
|
3405
|
+
}
|
|
3406
|
+
return [...images].sort();
|
|
3407
|
+
}
|
|
3408
|
+
function parseComposeConfigImages(stdout) {
|
|
3409
|
+
if (Buffer.byteLength(stdout, "utf8") > COMPOSE_CONFIG_JSON_MAX_BYTES) return null;
|
|
3410
|
+
try {
|
|
3411
|
+
const parsed = JSON.parse(stdout);
|
|
3412
|
+
if (!isRecord(parsed)) return null;
|
|
3413
|
+
const { services } = parsed;
|
|
3414
|
+
if (!isRecord(services)) return null;
|
|
3415
|
+
return collectComposeConfigImages(services);
|
|
3416
|
+
} catch {
|
|
3417
|
+
return null;
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
function buildComposeImageInspectCommand(runtime, image) {
|
|
3421
|
+
return `${runtime} image inspect --format '${COMPOSE_IMAGE_INSPECT_FORMAT}' -- ${shellQuote(image)}`;
|
|
3422
|
+
}
|
|
3423
|
+
function getComposeImageRepository(image) {
|
|
3424
|
+
const digestSeparatorIndex = image.indexOf("@");
|
|
3425
|
+
if (digestSeparatorIndex !== -1) return image.slice(0, digestSeparatorIndex);
|
|
3426
|
+
const lastSlashIndex = image.lastIndexOf("/");
|
|
3427
|
+
const lastColonIndex = image.lastIndexOf(":");
|
|
3428
|
+
return lastColonIndex > lastSlashIndex ? image.slice(0, lastColonIndex) : image;
|
|
3429
|
+
}
|
|
3430
|
+
function readComposeImageIdentifierFromInspectOutput(image, output) {
|
|
3431
|
+
const lines = output.split("\n").filter((line) => line.length > 0);
|
|
3432
|
+
if (lines.length === 0) return null;
|
|
3433
|
+
const [id, ...repoDigests] = lines;
|
|
3434
|
+
const repository = getComposeImageRepository(image);
|
|
3435
|
+
for (const repoDigest of repoDigests) {
|
|
3436
|
+
const separatorIndex = repoDigest.indexOf("@");
|
|
3437
|
+
if (separatorIndex === -1) continue;
|
|
3438
|
+
const repo = repoDigest.slice(0, separatorIndex);
|
|
3439
|
+
const digest = repoDigest.slice(separatorIndex + 1);
|
|
3440
|
+
if (repo === repository && digest.length > 0) return digest;
|
|
3441
|
+
}
|
|
3442
|
+
return id.length > 0 ? id : null;
|
|
3443
|
+
}
|
|
3444
|
+
async function resolveComposeConfigImages(parameters) {
|
|
3445
|
+
const result = await parameters.ssh.exec(
|
|
3446
|
+
`${composeCommand(parameters.runtime, parameters.projectDirectory)} config --format json`,
|
|
3447
|
+
EXEC_OPTS4
|
|
3448
|
+
);
|
|
3449
|
+
if (result.code !== 0) {
|
|
3450
|
+
return failedCommand(
|
|
3451
|
+
`[compose.pull] failed to resolve images for ${parameters.projectDirectory}`,
|
|
3452
|
+
result
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3455
|
+
const images = parseComposeConfigImages(result.stdout);
|
|
3456
|
+
if (images === null) {
|
|
3457
|
+
return failed(
|
|
3458
|
+
`[compose.pull] failed to parse compose config images for ${parameters.projectDirectory}`
|
|
3459
|
+
);
|
|
3460
|
+
}
|
|
3461
|
+
return images;
|
|
3462
|
+
}
|
|
3463
|
+
async function inspectComposeImageBeforePull(parameters) {
|
|
3464
|
+
const result = await parameters.ssh.exec(
|
|
3465
|
+
buildComposeImageInspectCommand(parameters.runtime, parameters.image),
|
|
3466
|
+
EXEC_OPTS4
|
|
3467
|
+
);
|
|
3468
|
+
if (result.code !== 0) return null;
|
|
3469
|
+
return readComposeImageIdentifierFromInspectOutput(parameters.image, result.stdout);
|
|
3470
|
+
}
|
|
3471
|
+
async function inspectComposeImageAfterPull(parameters) {
|
|
3472
|
+
const result = await parameters.ssh.exec(
|
|
3473
|
+
buildComposeImageInspectCommand(parameters.runtime, parameters.image),
|
|
3474
|
+
EXEC_OPTS4
|
|
3475
|
+
);
|
|
3476
|
+
if (result.code !== 0) {
|
|
3477
|
+
return failedCommand(
|
|
3478
|
+
`[compose.pull] image inspect failed for ${parameters.image} in ${parameters.projectDirectory}`,
|
|
3479
|
+
result
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
const imageIdentifier = readComposeImageIdentifierFromInspectOutput(
|
|
3483
|
+
parameters.image,
|
|
3484
|
+
result.stdout
|
|
3485
|
+
);
|
|
3486
|
+
if (imageIdentifier === null) {
|
|
3487
|
+
return failed(
|
|
3488
|
+
`[compose.pull] image inspect returned no digest or image ID for ${parameters.image} in ${parameters.projectDirectory}`
|
|
3489
|
+
);
|
|
3490
|
+
}
|
|
3491
|
+
return imageIdentifier;
|
|
3492
|
+
}
|
|
3493
|
+
async function snapshotComposeImagesBeforePull(parameters) {
|
|
3494
|
+
return new Map(
|
|
3495
|
+
await Promise.all(
|
|
3496
|
+
parameters.images.map(
|
|
3497
|
+
async (image) => [
|
|
3498
|
+
image,
|
|
3499
|
+
await inspectComposeImageBeforePull({
|
|
3500
|
+
image,
|
|
3501
|
+
runtime: parameters.runtime,
|
|
3502
|
+
ssh: parameters.ssh
|
|
3503
|
+
})
|
|
3504
|
+
]
|
|
3505
|
+
)
|
|
3506
|
+
)
|
|
3507
|
+
);
|
|
3508
|
+
}
|
|
3509
|
+
async function snapshotComposeImagesAfterPull(parameters) {
|
|
3510
|
+
const entries = await Promise.all(
|
|
3511
|
+
parameters.images.map(async (image) => ({
|
|
3512
|
+
image,
|
|
3513
|
+
imageIdentifier: await inspectComposeImageAfterPull({
|
|
3514
|
+
image,
|
|
3515
|
+
projectDirectory: parameters.projectDirectory,
|
|
3516
|
+
runtime: parameters.runtime,
|
|
3517
|
+
ssh: parameters.ssh
|
|
3518
|
+
})
|
|
3519
|
+
}))
|
|
3520
|
+
);
|
|
3521
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
3522
|
+
for (const { image, imageIdentifier } of entries) {
|
|
3523
|
+
if (typeof imageIdentifier !== "string") return imageIdentifier;
|
|
3524
|
+
snapshot.set(image, imageIdentifier);
|
|
3525
|
+
}
|
|
3526
|
+
return snapshot;
|
|
3527
|
+
}
|
|
3528
|
+
function composeImageSnapshotChanged(parameters) {
|
|
3529
|
+
for (const [image, afterIdentifier] of parameters.after) {
|
|
3530
|
+
if (parameters.before.get(image) !== afterIdentifier) return true;
|
|
3531
|
+
}
|
|
3532
|
+
return false;
|
|
3533
|
+
}
|
|
3395
3534
|
async function resolveComposeProjectName(parameters) {
|
|
3396
3535
|
const result = await parameters.ssh.exec(
|
|
3397
3536
|
`${composeCommand(parameters.runtime, parameters.projectDirectory)} config --format json`,
|
|
@@ -4050,13 +4189,33 @@ var compose = {
|
|
|
4050
4189
|
ssh: connection
|
|
4051
4190
|
});
|
|
4052
4191
|
if (typeof runtime !== "string") return runtime;
|
|
4192
|
+
const images = await resolveComposeConfigImages({
|
|
4193
|
+
projectDirectory,
|
|
4194
|
+
runtime,
|
|
4195
|
+
ssh: connection
|
|
4196
|
+
});
|
|
4197
|
+
if (!Array.isArray(images)) return images;
|
|
4198
|
+
const beforePull = await snapshotComposeImagesBeforePull({
|
|
4199
|
+
images,
|
|
4200
|
+
runtime,
|
|
4201
|
+
ssh: connection
|
|
4202
|
+
});
|
|
4053
4203
|
const result = await connection.exec(
|
|
4054
4204
|
`${composeCommand(runtime, projectDirectory)} pull 2>&1`,
|
|
4055
4205
|
EXEC_OPTS4
|
|
4056
4206
|
);
|
|
4057
4207
|
if (result.code !== 0)
|
|
4058
4208
|
return failedCommand(`[compose.pull] failed for ${projectDirectory}`, result);
|
|
4059
|
-
|
|
4209
|
+
const afterPull = await snapshotComposeImagesAfterPull({
|
|
4210
|
+
images,
|
|
4211
|
+
projectDirectory,
|
|
4212
|
+
runtime,
|
|
4213
|
+
ssh: connection
|
|
4214
|
+
});
|
|
4215
|
+
if (!(afterPull instanceof Map)) return afterPull;
|
|
4216
|
+
return {
|
|
4217
|
+
status: composeImageSnapshotChanged({ after: afterPull, before: beforePull }) ? "changed" : "ok"
|
|
4218
|
+
};
|
|
4060
4219
|
},
|
|
4061
4220
|
// eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
|
|
4062
4221
|
async check() {
|
|
@@ -6185,7 +6344,7 @@ function describeType(value) {
|
|
|
6185
6344
|
function isHostKeyMode(value) {
|
|
6186
6345
|
return value === "accept-new" || value === "no" || value === "yes";
|
|
6187
6346
|
}
|
|
6188
|
-
function
|
|
6347
|
+
function isRecord2(value) {
|
|
6189
6348
|
return Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null;
|
|
6190
6349
|
}
|
|
6191
6350
|
function collectOptionalBooleanErrors(object, key, errors) {
|
|
@@ -6304,7 +6463,7 @@ function collectSshConfigErrors(value) {
|
|
|
6304
6463
|
errors.push(`Invalid property 'ssh' (expected object, got ${describeType(value)})`);
|
|
6305
6464
|
return errors;
|
|
6306
6465
|
}
|
|
6307
|
-
if (!
|
|
6466
|
+
if (!isRecord2(value)) {
|
|
6308
6467
|
errors.push("Invalid property 'ssh' (expected object, got object)");
|
|
6309
6468
|
return errors;
|
|
6310
6469
|
}
|
|
@@ -6329,7 +6488,8 @@ function normalizeServerDefinitionSshError(error) {
|
|
|
6329
6488
|
function validateSshConfig(ssh2) {
|
|
6330
6489
|
const errors = collectSshConfigErrors(ssh2);
|
|
6331
6490
|
if (errors.length === 0) return;
|
|
6332
|
-
|
|
6491
|
+
const message = errors.map((error) => normalizeServerDefinitionSshError(error)).join("; ");
|
|
6492
|
+
throw new Error(`ServerDefinition: ${message}`);
|
|
6333
6493
|
}
|
|
6334
6494
|
|
|
6335
6495
|
// src/meta.ts
|
|
@@ -6372,7 +6532,7 @@ function normalizeMetaValueResolver(value) {
|
|
|
6372
6532
|
return value;
|
|
6373
6533
|
};
|
|
6374
6534
|
}
|
|
6375
|
-
function
|
|
6535
|
+
function isRecord3(value) {
|
|
6376
6536
|
return typeof value === "object" && value !== null;
|
|
6377
6537
|
}
|
|
6378
6538
|
function assertValidEnvironmentMetaEntry(candidate) {
|
|
@@ -6460,7 +6620,7 @@ function isSystemRebootMetaEntry(entry) {
|
|
|
6460
6620
|
return entry.kind === SYSTEM_REBOOT_KIND;
|
|
6461
6621
|
}
|
|
6462
6622
|
function assertValidModuleMetaEntry(entry) {
|
|
6463
|
-
if (!
|
|
6623
|
+
if (!isRecord3(entry)) {
|
|
6464
6624
|
throw new TypeError(`Invalid meta entry: expected object, got ${typeof entry}`);
|
|
6465
6625
|
}
|
|
6466
6626
|
switch (entry.kind) {
|
|
@@ -13000,7 +13160,20 @@ async function ensureSshDirectoryAndAuthorizedKeysAreNotSymlinks(conn, parameter
|
|
|
13000
13160
|
const keysPath = shellQuote(authorizedKeysPath);
|
|
13001
13161
|
const quotedUser = shellQuote(user2);
|
|
13002
13162
|
const quotedPrimaryGroup = shellQuote(primaryGroup);
|
|
13003
|
-
const command2 = `set -e
|
|
13163
|
+
const command2 = `set -e
|
|
13164
|
+
[ ! -L ${directory} ] || { echo '.ssh must not be a symlink' >&2; exit 1; }
|
|
13165
|
+
if [ -e ${directory} ]; then
|
|
13166
|
+
[ -d ${directory} ] || { echo '.ssh must be a directory' >&2; exit 1; }
|
|
13167
|
+
else
|
|
13168
|
+
mkdir -p ${directory}
|
|
13169
|
+
fi
|
|
13170
|
+
[ -d ${directory} ] && [ ! -L ${directory} ] || { echo '.ssh must be a real directory' >&2; exit 1; }
|
|
13171
|
+
ssh_directory_identity=$(stat -c '%d:%i' ${directory}) || exit $?
|
|
13172
|
+
chmod 700 ${directory} && chown ${quotedUser}:${quotedPrimaryGroup} ${directory}
|
|
13173
|
+
[ ! -L ${directory} ] && [ -d ${directory} ] || { echo '.ssh must be a real directory' >&2; exit 1; }
|
|
13174
|
+
post_ssh_directory_identity=$(stat -c '%d:%i' ${directory}) || exit $?
|
|
13175
|
+
[ "$post_ssh_directory_identity" = "$ssh_directory_identity" ] || { echo '.ssh directory changed during metadata update' >&2; exit 1; }
|
|
13176
|
+
[ ! -L ${keysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }`;
|
|
13004
13177
|
const result = await conn.exec(command2, MUTATION_EXEC_OPTS);
|
|
13005
13178
|
if (result.code !== 0) {
|
|
13006
13179
|
const label = result.stderr.includes("authorized_keys must not be a symlink") ? "authorized_keys symlink check failed" : "failed to prepare .ssh directory";
|
|
@@ -13030,10 +13203,25 @@ async function stageAuthorizedKeysContent(conn, parameters) {
|
|
|
13030
13203
|
const filterScratchPath = `${temporaryPath}.filter`;
|
|
13031
13204
|
const quotedFilterScratchPath = shellQuote(filterScratchPath);
|
|
13032
13205
|
const quotedKey = shellQuote(key);
|
|
13033
|
-
const existingAuthorizedKeysGuard = `[ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13206
|
+
const existingAuthorizedKeysGuard = `[ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13207
|
+
[ -f ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must be a regular file' >&2; exit 1; }`;
|
|
13034
13208
|
const readExistingAuthorizedKeys = `dd if=${quotedAuthorizedKeysPath} iflag=nofollow status=none of=${quotedTemporaryPath}`;
|
|
13035
13209
|
const stage = state === "present" ? await conn.exec(
|
|
13036
|
-
`{ if [ -e ${quotedAuthorizedKeysPath} ]; then
|
|
13210
|
+
`{ if [ -e ${quotedAuthorizedKeysPath} ]; then
|
|
13211
|
+
${existingAuthorizedKeysGuard}
|
|
13212
|
+
${readExistingAuthorizedKeys} || exit $?
|
|
13213
|
+
grep -qxF -- ${quotedKey} ${quotedTemporaryPath}
|
|
13214
|
+
grep_status=$?
|
|
13215
|
+
if [ "$grep_status" -eq 0 ]; then
|
|
13216
|
+
:
|
|
13217
|
+
elif [ "$grep_status" -eq 1 ]; then
|
|
13218
|
+
printf '%s\\n' ${quotedKey} >> ${quotedTemporaryPath}
|
|
13219
|
+
else
|
|
13220
|
+
exit "$grep_status"
|
|
13221
|
+
fi
|
|
13222
|
+
else
|
|
13223
|
+
printf '%s\\n' ${quotedKey} > ${quotedTemporaryPath}
|
|
13224
|
+
fi; }`,
|
|
13037
13225
|
MUTATION_EXEC_OPTS
|
|
13038
13226
|
) : (
|
|
13039
13227
|
// R-0000044: use `grep -vxF` (whole-line match) to mirror the present
|
|
@@ -13045,7 +13233,20 @@ async function stageAuthorizedKeysContent(conn, parameters) {
|
|
|
13045
13233
|
// TOCTOU where the path could become a symlink between the
|
|
13046
13234
|
// `[ ! -L ]` guard and grep's open() call.
|
|
13047
13235
|
await conn.exec(
|
|
13048
|
-
`{ if [ -e ${quotedAuthorizedKeysPath} ]; then
|
|
13236
|
+
`{ if [ -e ${quotedAuthorizedKeysPath} ]; then
|
|
13237
|
+
${existingAuthorizedKeysGuard}
|
|
13238
|
+
${readExistingAuthorizedKeys} || exit $?
|
|
13239
|
+
grep -vxF -- ${quotedKey} ${quotedTemporaryPath} > ${quotedFilterScratchPath}
|
|
13240
|
+
grep_status=$?
|
|
13241
|
+
if [ "$grep_status" -eq 0 ] || [ "$grep_status" -eq 1 ]; then
|
|
13242
|
+
mv -T -- ${quotedFilterScratchPath} ${quotedTemporaryPath} || exit $?
|
|
13243
|
+
else
|
|
13244
|
+
rm -f -- ${quotedFilterScratchPath}
|
|
13245
|
+
exit "$grep_status"
|
|
13246
|
+
fi
|
|
13247
|
+
else
|
|
13248
|
+
: > ${quotedTemporaryPath}
|
|
13249
|
+
fi; }`,
|
|
13049
13250
|
MUTATION_EXEC_OPTS
|
|
13050
13251
|
)
|
|
13051
13252
|
);
|
|
@@ -13066,7 +13267,38 @@ async function replaceAuthorizedKeysAtomically(conn, parameters) {
|
|
|
13066
13267
|
const expectedAuthorizedKeysState = shellQuote(`600 ${user2} ${primaryGroup} regular file`);
|
|
13067
13268
|
const quotedBackupPath = shellQuote(`${authorizedKeysPath}.paratix-backup`);
|
|
13068
13269
|
const replace2 = await conn.exec(
|
|
13069
|
-
`chmod 600 ${quotedTemporaryPath} && chown ${shellQuote(user2)}:${shellQuote(primaryGroup)} ${quotedTemporaryPath} && {
|
|
13270
|
+
`chmod 600 ${quotedTemporaryPath} && chown ${shellQuote(user2)}:${shellQuote(primaryGroup)} ${quotedTemporaryPath} && {
|
|
13271
|
+
expected_authorized_keys_hash=$(sha256sum ${quotedTemporaryPath} | cut -d' ' -f1) || exit $?
|
|
13272
|
+
[ ! -L ${quotedSshDirectoryPath} ] || { echo '.ssh must not be a symlink' >&2; exit 1; }
|
|
13273
|
+
[ -d ${quotedSshDirectoryPath} ] || { echo '.ssh must be a directory' >&2; exit 1; }
|
|
13274
|
+
ssh_directory_state=$(stat -c '%a %U %G %F' ${quotedSshDirectoryPath}) || exit $?
|
|
13275
|
+
[ "$ssh_directory_state" = ${expectedSshDirectoryState} ] || { echo '.ssh ownership changed before authorized_keys replace' >&2; exit 1; }
|
|
13276
|
+
[ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13277
|
+
rm -f -- ${quotedBackupPath}
|
|
13278
|
+
backup_created=0
|
|
13279
|
+
if [ -e ${quotedAuthorizedKeysPath} ]; then
|
|
13280
|
+
[ -f ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must be a regular file' >&2; exit 1; }
|
|
13281
|
+
[ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13282
|
+
ln -P -- ${quotedAuthorizedKeysPath} ${quotedBackupPath} || { echo 'failed to create authorized_keys backup hardlink' >&2; exit 1; }
|
|
13283
|
+
backup_created=1
|
|
13284
|
+
fi
|
|
13285
|
+
[ ! -L ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13286
|
+
if ! mv -T -- ${quotedTemporaryPath} ${quotedAuthorizedKeysPath}; then
|
|
13287
|
+
if [ "$backup_created" = 1 ]; then
|
|
13288
|
+
mv -T -- ${quotedBackupPath} ${quotedAuthorizedKeysPath} || echo 'authorized_keys backup restore failed; backup is at '${quotedBackupPath} >&2
|
|
13289
|
+
fi
|
|
13290
|
+
echo 'authorized_keys replace failed' >&2
|
|
13291
|
+
exit 1
|
|
13292
|
+
fi
|
|
13293
|
+
[ ! -e ${quotedTemporaryPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys replace did not consume temporary file' >&2; exit 1; }
|
|
13294
|
+
[ ! -L ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13295
|
+
[ -f ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must be a regular file' >&2; exit 1; }
|
|
13296
|
+
authorized_keys_state=$(stat -c '%a %U %G %F' ${quotedAuthorizedKeysPath}) || { rm -f -- ${quotedBackupPath}; exit 1; }
|
|
13297
|
+
[ "$authorized_keys_state" = ${expectedAuthorizedKeysState} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys metadata changed during replace' >&2; exit 1; }
|
|
13298
|
+
authorized_keys_hash=$(sha256sum ${quotedAuthorizedKeysPath} | cut -d' ' -f1) || { rm -f -- ${quotedBackupPath}; exit 1; }
|
|
13299
|
+
[ "$authorized_keys_hash" = "$expected_authorized_keys_hash" ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys content changed during replace' >&2; exit 1; }
|
|
13300
|
+
rm -f -- ${quotedBackupPath}
|
|
13301
|
+
}`,
|
|
13070
13302
|
MUTATION_EXEC_OPTS
|
|
13071
13303
|
);
|
|
13072
13304
|
if (replace2.code !== 0) {
|
|
@@ -19070,4 +19302,3 @@ export {
|
|
|
19070
19302
|
ufw,
|
|
19071
19303
|
user
|
|
19072
19304
|
};
|
|
19073
|
-
//# sourceMappingURL=chunk-M3ZQJNKM.js.map
|