paratix 0.12.6 → 0.13.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 +5 -1
- package/dist/{chunk-CWGQ2MYN.js → chunk-XIRORNO3.js} +252 -25
- package/dist/cli.js +2259 -1505
- package/dist/{index-udpAybq3.d.ts → index-DwKdTyHo.d.ts} +25 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.js +56 -29
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +1 -2
- package/llm-guide.md +17 -17
- package/package.json +5 -3
- package/dist/chunk-CWGQ2MYN.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
|
@@ -127,17 +127,21 @@ Options:
|
|
|
127
127
|
--dry-run
|
|
128
128
|
--env <key=value>
|
|
129
129
|
--env-file <path>
|
|
130
|
+
--filter <names>
|
|
130
131
|
--first-run
|
|
131
132
|
--reconnect-timeout <seconds>
|
|
132
133
|
--verbose
|
|
133
|
-
--version
|
|
134
134
|
--help
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
`--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.
|
|
138
|
+
|
|
137
139
|
`--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
140
|
|
|
139
141
|
`--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
142
|
|
|
143
|
+
`--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.
|
|
144
|
+
|
|
141
145
|
## Documentation
|
|
142
146
|
|
|
143
147
|
- For project scaffolding, see [`create-paratix`](https://www.npmjs.com/package/create-paratix).
|
|
@@ -648,6 +648,8 @@ import { Readable } from "stream";
|
|
|
648
648
|
// src/terminal.ts
|
|
649
649
|
import { createInterface } from "readline";
|
|
650
650
|
import { Writable } from "stream";
|
|
651
|
+
var ASCII_ESC = 27;
|
|
652
|
+
var ANSI_SHOW_CURSOR = `${String.fromCharCode(ASCII_ESC)}[?25h`;
|
|
651
653
|
|
|
652
654
|
// src/ssh.ts
|
|
653
655
|
function assertValidMktempDirectory(directory) {
|
|
@@ -1566,13 +1568,6 @@ function buildRepositoryUpdateFlag(name, expectedContent) {
|
|
|
1566
1568
|
flagPrefix
|
|
1567
1569
|
};
|
|
1568
1570
|
}
|
|
1569
|
-
function firstNonEmptyApt(text) {
|
|
1570
|
-
for (const line of text.split("\n")) {
|
|
1571
|
-
const trimmed = line.trim();
|
|
1572
|
-
if (trimmed.length > 0) return trimmed;
|
|
1573
|
-
}
|
|
1574
|
-
return null;
|
|
1575
|
-
}
|
|
1576
1571
|
async function probeAptRepositoryInodeIdentity(ssh2, filePath) {
|
|
1577
1572
|
const result = await ssh2.exec(`stat -c '%d:%i' ${shellQuote(filePath)}`, {
|
|
1578
1573
|
ignoreExitCode: true,
|
|
@@ -1613,7 +1608,7 @@ async function rollbackRepositoryAfterUpdateFailure(parameters) {
|
|
|
1613
1608
|
const failureMessage = `[apt.repository] apt-get update failed for ${name}`;
|
|
1614
1609
|
if (rollbackUpdate.code !== 0) {
|
|
1615
1610
|
return failedCommand(
|
|
1616
|
-
`${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"}`,
|
|
1617
1612
|
updateResult
|
|
1618
1613
|
);
|
|
1619
1614
|
}
|
|
@@ -3321,10 +3316,12 @@ var EXEC_OPTS4 = { ignoreExitCode: true, silent: true };
|
|
|
3321
3316
|
var UNIT_NAME_PATTERN = new RegExp("^[\\w@.\\-]+$", "v");
|
|
3322
3317
|
var COMPOSE_CONFIG_MODE = "0600";
|
|
3323
3318
|
var COMPOSE_CONFIG_STAGING_PREFIX = ".compose.yml.paratix-staging";
|
|
3319
|
+
var COMPOSE_IMAGE_INSPECT_FORMAT = "{{.Id}}\\n{{range .RepoDigests}}{{.}}\\n{{end}}";
|
|
3324
3320
|
var SYSTEMD_UNIT_MODE = "0644";
|
|
3325
3321
|
var SYSTEMD_UNIT_STAGING_PREFIX = ".compose-systemd-unit.paratix-staging";
|
|
3326
3322
|
var C0_CONTROL_MAX_CODE_POINT = 31;
|
|
3327
3323
|
var DELETE_CONTROL_CODE_POINT2 = 127;
|
|
3324
|
+
var COMPOSE_CONFIG_JSON_MAX_BYTES = 1048576;
|
|
3328
3325
|
function requireComposeSsh(ssh2, action, projectDirectory) {
|
|
3329
3326
|
return ssh2 ?? failed(`[compose.${action}] SSH connection is required for ${projectDirectory}`);
|
|
3330
3327
|
}
|
|
@@ -3349,10 +3346,6 @@ var COMPOSE_UP_ACTION_KEYWORDS_REGEX = new RegExp("^(?:Creating|Recreating|Start
|
|
|
3349
3346
|
function composeUpReportedChange(composeOutput) {
|
|
3350
3347
|
return COMPOSE_UP_ACTION_KEYWORDS_REGEX.test(composeOutput);
|
|
3351
3348
|
}
|
|
3352
|
-
var COMPOSE_PULL_ACTION_REGEX = new RegExp("^(?:Pulling|Downloaded)\\s", "mv");
|
|
3353
|
-
function composePullReportedChange(composeOutput) {
|
|
3354
|
-
return COMPOSE_PULL_ACTION_REGEX.test(composeOutput);
|
|
3355
|
-
}
|
|
3356
3349
|
function validateComposeUpServices(services) {
|
|
3357
3350
|
for (const service2 of services ?? []) {
|
|
3358
3351
|
if (service2 === "") {
|
|
@@ -3376,7 +3369,7 @@ async function getRuntime(ssh2, action, explicit) {
|
|
|
3376
3369
|
return detectRuntime(ssh2);
|
|
3377
3370
|
}
|
|
3378
3371
|
function composeCommand(runtime, projectDirectory) {
|
|
3379
|
-
return
|
|
3372
|
+
return `cd ${shellQuote(projectDirectory)} && ${runtime} compose`;
|
|
3380
3373
|
}
|
|
3381
3374
|
function parseComposeProjectName(stdout, projectDirectory) {
|
|
3382
3375
|
try {
|
|
@@ -3390,6 +3383,148 @@ function parseComposeProjectName(stdout, projectDirectory) {
|
|
|
3390
3383
|
return null;
|
|
3391
3384
|
}
|
|
3392
3385
|
}
|
|
3386
|
+
function isRecord(value) {
|
|
3387
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3388
|
+
}
|
|
3389
|
+
function readComposeServiceImage(service2) {
|
|
3390
|
+
if (!isRecord(service2)) return null;
|
|
3391
|
+
const { image } = service2;
|
|
3392
|
+
return typeof image === "string" && image.trim() !== "" ? image : null;
|
|
3393
|
+
}
|
|
3394
|
+
function collectComposeConfigImages(services) {
|
|
3395
|
+
const images = /* @__PURE__ */ new Set();
|
|
3396
|
+
for (const service2 of Object.values(services)) {
|
|
3397
|
+
const image = readComposeServiceImage(service2);
|
|
3398
|
+
if (image !== null) images.add(image);
|
|
3399
|
+
}
|
|
3400
|
+
return [...images].sort();
|
|
3401
|
+
}
|
|
3402
|
+
function parseComposeConfigImages(stdout) {
|
|
3403
|
+
if (Buffer.byteLength(stdout, "utf8") > COMPOSE_CONFIG_JSON_MAX_BYTES) return null;
|
|
3404
|
+
try {
|
|
3405
|
+
const parsed = JSON.parse(stdout);
|
|
3406
|
+
if (!isRecord(parsed)) return null;
|
|
3407
|
+
const { services } = parsed;
|
|
3408
|
+
if (!isRecord(services)) return null;
|
|
3409
|
+
return collectComposeConfigImages(services);
|
|
3410
|
+
} catch {
|
|
3411
|
+
return null;
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
function buildComposeImageInspectCommand(runtime, image) {
|
|
3415
|
+
return `${runtime} image inspect --format '${COMPOSE_IMAGE_INSPECT_FORMAT}' -- ${shellQuote(image)}`;
|
|
3416
|
+
}
|
|
3417
|
+
function getComposeImageRepository(image) {
|
|
3418
|
+
const digestSeparatorIndex = image.indexOf("@");
|
|
3419
|
+
if (digestSeparatorIndex !== -1) return image.slice(0, digestSeparatorIndex);
|
|
3420
|
+
const lastSlashIndex = image.lastIndexOf("/");
|
|
3421
|
+
const lastColonIndex = image.lastIndexOf(":");
|
|
3422
|
+
return lastColonIndex > lastSlashIndex ? image.slice(0, lastColonIndex) : image;
|
|
3423
|
+
}
|
|
3424
|
+
function readComposeImageIdentifierFromInspectOutput(image, output) {
|
|
3425
|
+
const lines = output.split("\n").filter((line) => line.length > 0);
|
|
3426
|
+
if (lines.length === 0) return null;
|
|
3427
|
+
const [id, ...repoDigests] = lines;
|
|
3428
|
+
const repository = getComposeImageRepository(image);
|
|
3429
|
+
for (const repoDigest of repoDigests) {
|
|
3430
|
+
const separatorIndex = repoDigest.indexOf("@");
|
|
3431
|
+
if (separatorIndex === -1) continue;
|
|
3432
|
+
const repo = repoDigest.slice(0, separatorIndex);
|
|
3433
|
+
const digest = repoDigest.slice(separatorIndex + 1);
|
|
3434
|
+
if (repo === repository && digest.length > 0) return digest;
|
|
3435
|
+
}
|
|
3436
|
+
return id.length > 0 ? id : null;
|
|
3437
|
+
}
|
|
3438
|
+
async function resolveComposeConfigImages(parameters) {
|
|
3439
|
+
const result = await parameters.ssh.exec(
|
|
3440
|
+
`${composeCommand(parameters.runtime, parameters.projectDirectory)} config --format json`,
|
|
3441
|
+
EXEC_OPTS4
|
|
3442
|
+
);
|
|
3443
|
+
if (result.code !== 0) {
|
|
3444
|
+
return failedCommand(
|
|
3445
|
+
`[compose.pull] failed to resolve images for ${parameters.projectDirectory}`,
|
|
3446
|
+
result
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
const images = parseComposeConfigImages(result.stdout);
|
|
3450
|
+
if (images === null) {
|
|
3451
|
+
return failed(
|
|
3452
|
+
`[compose.pull] failed to parse compose config images for ${parameters.projectDirectory}`
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3455
|
+
return images;
|
|
3456
|
+
}
|
|
3457
|
+
async function inspectComposeImageBeforePull(parameters) {
|
|
3458
|
+
const result = await parameters.ssh.exec(
|
|
3459
|
+
buildComposeImageInspectCommand(parameters.runtime, parameters.image),
|
|
3460
|
+
EXEC_OPTS4
|
|
3461
|
+
);
|
|
3462
|
+
if (result.code !== 0) return null;
|
|
3463
|
+
return readComposeImageIdentifierFromInspectOutput(parameters.image, result.stdout);
|
|
3464
|
+
}
|
|
3465
|
+
async function inspectComposeImageAfterPull(parameters) {
|
|
3466
|
+
const result = await parameters.ssh.exec(
|
|
3467
|
+
buildComposeImageInspectCommand(parameters.runtime, parameters.image),
|
|
3468
|
+
EXEC_OPTS4
|
|
3469
|
+
);
|
|
3470
|
+
if (result.code !== 0) {
|
|
3471
|
+
return failedCommand(
|
|
3472
|
+
`[compose.pull] image inspect failed for ${parameters.image} in ${parameters.projectDirectory}`,
|
|
3473
|
+
result
|
|
3474
|
+
);
|
|
3475
|
+
}
|
|
3476
|
+
const imageIdentifier = readComposeImageIdentifierFromInspectOutput(
|
|
3477
|
+
parameters.image,
|
|
3478
|
+
result.stdout
|
|
3479
|
+
);
|
|
3480
|
+
if (imageIdentifier === null) {
|
|
3481
|
+
return failed(
|
|
3482
|
+
`[compose.pull] image inspect returned no digest or image ID for ${parameters.image} in ${parameters.projectDirectory}`
|
|
3483
|
+
);
|
|
3484
|
+
}
|
|
3485
|
+
return imageIdentifier;
|
|
3486
|
+
}
|
|
3487
|
+
async function snapshotComposeImagesBeforePull(parameters) {
|
|
3488
|
+
return new Map(
|
|
3489
|
+
await Promise.all(
|
|
3490
|
+
parameters.images.map(
|
|
3491
|
+
async (image) => [
|
|
3492
|
+
image,
|
|
3493
|
+
await inspectComposeImageBeforePull({
|
|
3494
|
+
image,
|
|
3495
|
+
runtime: parameters.runtime,
|
|
3496
|
+
ssh: parameters.ssh
|
|
3497
|
+
})
|
|
3498
|
+
]
|
|
3499
|
+
)
|
|
3500
|
+
)
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
async function snapshotComposeImagesAfterPull(parameters) {
|
|
3504
|
+
const entries = await Promise.all(
|
|
3505
|
+
parameters.images.map(async (image) => ({
|
|
3506
|
+
image,
|
|
3507
|
+
imageIdentifier: await inspectComposeImageAfterPull({
|
|
3508
|
+
image,
|
|
3509
|
+
projectDirectory: parameters.projectDirectory,
|
|
3510
|
+
runtime: parameters.runtime,
|
|
3511
|
+
ssh: parameters.ssh
|
|
3512
|
+
})
|
|
3513
|
+
}))
|
|
3514
|
+
);
|
|
3515
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
3516
|
+
for (const { image, imageIdentifier } of entries) {
|
|
3517
|
+
if (typeof imageIdentifier !== "string") return imageIdentifier;
|
|
3518
|
+
snapshot.set(image, imageIdentifier);
|
|
3519
|
+
}
|
|
3520
|
+
return snapshot;
|
|
3521
|
+
}
|
|
3522
|
+
function composeImageSnapshotChanged(parameters) {
|
|
3523
|
+
for (const [image, afterIdentifier] of parameters.after) {
|
|
3524
|
+
if (parameters.before.get(image) !== afterIdentifier) return true;
|
|
3525
|
+
}
|
|
3526
|
+
return false;
|
|
3527
|
+
}
|
|
3393
3528
|
async function resolveComposeProjectName(parameters) {
|
|
3394
3529
|
const result = await parameters.ssh.exec(
|
|
3395
3530
|
`${composeCommand(parameters.runtime, parameters.projectDirectory)} config --format json`,
|
|
@@ -4048,13 +4183,33 @@ var compose = {
|
|
|
4048
4183
|
ssh: connection
|
|
4049
4184
|
});
|
|
4050
4185
|
if (typeof runtime !== "string") return runtime;
|
|
4186
|
+
const images = await resolveComposeConfigImages({
|
|
4187
|
+
projectDirectory,
|
|
4188
|
+
runtime,
|
|
4189
|
+
ssh: connection
|
|
4190
|
+
});
|
|
4191
|
+
if (!Array.isArray(images)) return images;
|
|
4192
|
+
const beforePull = await snapshotComposeImagesBeforePull({
|
|
4193
|
+
images,
|
|
4194
|
+
runtime,
|
|
4195
|
+
ssh: connection
|
|
4196
|
+
});
|
|
4051
4197
|
const result = await connection.exec(
|
|
4052
4198
|
`${composeCommand(runtime, projectDirectory)} pull 2>&1`,
|
|
4053
4199
|
EXEC_OPTS4
|
|
4054
4200
|
);
|
|
4055
4201
|
if (result.code !== 0)
|
|
4056
4202
|
return failedCommand(`[compose.pull] failed for ${projectDirectory}`, result);
|
|
4057
|
-
|
|
4203
|
+
const afterPull = await snapshotComposeImagesAfterPull({
|
|
4204
|
+
images,
|
|
4205
|
+
projectDirectory,
|
|
4206
|
+
runtime,
|
|
4207
|
+
ssh: connection
|
|
4208
|
+
});
|
|
4209
|
+
if (!(afterPull instanceof Map)) return afterPull;
|
|
4210
|
+
return {
|
|
4211
|
+
status: composeImageSnapshotChanged({ after: afterPull, before: beforePull }) ? "changed" : "ok"
|
|
4212
|
+
};
|
|
4058
4213
|
},
|
|
4059
4214
|
// eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
|
|
4060
4215
|
async check() {
|
|
@@ -6183,7 +6338,7 @@ function describeType(value) {
|
|
|
6183
6338
|
function isHostKeyMode(value) {
|
|
6184
6339
|
return value === "accept-new" || value === "no" || value === "yes";
|
|
6185
6340
|
}
|
|
6186
|
-
function
|
|
6341
|
+
function isRecord2(value) {
|
|
6187
6342
|
return Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null;
|
|
6188
6343
|
}
|
|
6189
6344
|
function collectOptionalBooleanErrors(object, key, errors) {
|
|
@@ -6302,7 +6457,7 @@ function collectSshConfigErrors(value) {
|
|
|
6302
6457
|
errors.push(`Invalid property 'ssh' (expected object, got ${describeType(value)})`);
|
|
6303
6458
|
return errors;
|
|
6304
6459
|
}
|
|
6305
|
-
if (!
|
|
6460
|
+
if (!isRecord2(value)) {
|
|
6306
6461
|
errors.push("Invalid property 'ssh' (expected object, got object)");
|
|
6307
6462
|
return errors;
|
|
6308
6463
|
}
|
|
@@ -6327,7 +6482,8 @@ function normalizeServerDefinitionSshError(error) {
|
|
|
6327
6482
|
function validateSshConfig(ssh2) {
|
|
6328
6483
|
const errors = collectSshConfigErrors(ssh2);
|
|
6329
6484
|
if (errors.length === 0) return;
|
|
6330
|
-
|
|
6485
|
+
const message = errors.map((error) => normalizeServerDefinitionSshError(error)).join("; ");
|
|
6486
|
+
throw new Error(`ServerDefinition: ${message}`);
|
|
6331
6487
|
}
|
|
6332
6488
|
|
|
6333
6489
|
// src/meta.ts
|
|
@@ -6370,7 +6526,7 @@ function normalizeMetaValueResolver(value) {
|
|
|
6370
6526
|
return value;
|
|
6371
6527
|
};
|
|
6372
6528
|
}
|
|
6373
|
-
function
|
|
6529
|
+
function isRecord3(value) {
|
|
6374
6530
|
return typeof value === "object" && value !== null;
|
|
6375
6531
|
}
|
|
6376
6532
|
function assertValidEnvironmentMetaEntry(candidate) {
|
|
@@ -6458,7 +6614,7 @@ function isSystemRebootMetaEntry(entry) {
|
|
|
6458
6614
|
return entry.kind === SYSTEM_REBOOT_KIND;
|
|
6459
6615
|
}
|
|
6460
6616
|
function assertValidModuleMetaEntry(entry) {
|
|
6461
|
-
if (!
|
|
6617
|
+
if (!isRecord3(entry)) {
|
|
6462
6618
|
throw new TypeError(`Invalid meta entry: expected object, got ${typeof entry}`);
|
|
6463
6619
|
}
|
|
6464
6620
|
switch (entry.kind) {
|
|
@@ -12998,7 +13154,20 @@ async function ensureSshDirectoryAndAuthorizedKeysAreNotSymlinks(conn, parameter
|
|
|
12998
13154
|
const keysPath = shellQuote(authorizedKeysPath);
|
|
12999
13155
|
const quotedUser = shellQuote(user2);
|
|
13000
13156
|
const quotedPrimaryGroup = shellQuote(primaryGroup);
|
|
13001
|
-
const command2 = `set -e
|
|
13157
|
+
const command2 = `set -e
|
|
13158
|
+
[ ! -L ${directory} ] || { echo '.ssh must not be a symlink' >&2; exit 1; }
|
|
13159
|
+
if [ -e ${directory} ]; then
|
|
13160
|
+
[ -d ${directory} ] || { echo '.ssh must be a directory' >&2; exit 1; }
|
|
13161
|
+
else
|
|
13162
|
+
mkdir -p ${directory}
|
|
13163
|
+
fi
|
|
13164
|
+
[ -d ${directory} ] && [ ! -L ${directory} ] || { echo '.ssh must be a real directory' >&2; exit 1; }
|
|
13165
|
+
ssh_directory_identity=$(stat -c '%d:%i' ${directory}) || exit $?
|
|
13166
|
+
chmod 700 ${directory} && chown ${quotedUser}:${quotedPrimaryGroup} ${directory}
|
|
13167
|
+
[ ! -L ${directory} ] && [ -d ${directory} ] || { echo '.ssh must be a real directory' >&2; exit 1; }
|
|
13168
|
+
post_ssh_directory_identity=$(stat -c '%d:%i' ${directory}) || exit $?
|
|
13169
|
+
[ "$post_ssh_directory_identity" = "$ssh_directory_identity" ] || { echo '.ssh directory changed during metadata update' >&2; exit 1; }
|
|
13170
|
+
[ ! -L ${keysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }`;
|
|
13002
13171
|
const result = await conn.exec(command2, MUTATION_EXEC_OPTS);
|
|
13003
13172
|
if (result.code !== 0) {
|
|
13004
13173
|
const label = result.stderr.includes("authorized_keys must not be a symlink") ? "authorized_keys symlink check failed" : "failed to prepare .ssh directory";
|
|
@@ -13028,10 +13197,25 @@ async function stageAuthorizedKeysContent(conn, parameters) {
|
|
|
13028
13197
|
const filterScratchPath = `${temporaryPath}.filter`;
|
|
13029
13198
|
const quotedFilterScratchPath = shellQuote(filterScratchPath);
|
|
13030
13199
|
const quotedKey = shellQuote(key);
|
|
13031
|
-
const existingAuthorizedKeysGuard = `[ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13200
|
+
const existingAuthorizedKeysGuard = `[ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13201
|
+
[ -f ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must be a regular file' >&2; exit 1; }`;
|
|
13032
13202
|
const readExistingAuthorizedKeys = `dd if=${quotedAuthorizedKeysPath} iflag=nofollow status=none of=${quotedTemporaryPath}`;
|
|
13033
13203
|
const stage = state === "present" ? await conn.exec(
|
|
13034
|
-
`{ if [ -e ${quotedAuthorizedKeysPath} ]; then
|
|
13204
|
+
`{ if [ -e ${quotedAuthorizedKeysPath} ]; then
|
|
13205
|
+
${existingAuthorizedKeysGuard}
|
|
13206
|
+
${readExistingAuthorizedKeys} || exit $?
|
|
13207
|
+
grep -qxF -- ${quotedKey} ${quotedTemporaryPath}
|
|
13208
|
+
grep_status=$?
|
|
13209
|
+
if [ "$grep_status" -eq 0 ]; then
|
|
13210
|
+
:
|
|
13211
|
+
elif [ "$grep_status" -eq 1 ]; then
|
|
13212
|
+
printf '%s\\n' ${quotedKey} >> ${quotedTemporaryPath}
|
|
13213
|
+
else
|
|
13214
|
+
exit "$grep_status"
|
|
13215
|
+
fi
|
|
13216
|
+
else
|
|
13217
|
+
printf '%s\\n' ${quotedKey} > ${quotedTemporaryPath}
|
|
13218
|
+
fi; }`,
|
|
13035
13219
|
MUTATION_EXEC_OPTS
|
|
13036
13220
|
) : (
|
|
13037
13221
|
// R-0000044: use `grep -vxF` (whole-line match) to mirror the present
|
|
@@ -13043,7 +13227,20 @@ async function stageAuthorizedKeysContent(conn, parameters) {
|
|
|
13043
13227
|
// TOCTOU where the path could become a symlink between the
|
|
13044
13228
|
// `[ ! -L ]` guard and grep's open() call.
|
|
13045
13229
|
await conn.exec(
|
|
13046
|
-
`{ if [ -e ${quotedAuthorizedKeysPath} ]; then
|
|
13230
|
+
`{ if [ -e ${quotedAuthorizedKeysPath} ]; then
|
|
13231
|
+
${existingAuthorizedKeysGuard}
|
|
13232
|
+
${readExistingAuthorizedKeys} || exit $?
|
|
13233
|
+
grep -vxF -- ${quotedKey} ${quotedTemporaryPath} > ${quotedFilterScratchPath}
|
|
13234
|
+
grep_status=$?
|
|
13235
|
+
if [ "$grep_status" -eq 0 ] || [ "$grep_status" -eq 1 ]; then
|
|
13236
|
+
mv -T -- ${quotedFilterScratchPath} ${quotedTemporaryPath} || exit $?
|
|
13237
|
+
else
|
|
13238
|
+
rm -f -- ${quotedFilterScratchPath}
|
|
13239
|
+
exit "$grep_status"
|
|
13240
|
+
fi
|
|
13241
|
+
else
|
|
13242
|
+
: > ${quotedTemporaryPath}
|
|
13243
|
+
fi; }`,
|
|
13047
13244
|
MUTATION_EXEC_OPTS
|
|
13048
13245
|
)
|
|
13049
13246
|
);
|
|
@@ -13064,7 +13261,38 @@ async function replaceAuthorizedKeysAtomically(conn, parameters) {
|
|
|
13064
13261
|
const expectedAuthorizedKeysState = shellQuote(`600 ${user2} ${primaryGroup} regular file`);
|
|
13065
13262
|
const quotedBackupPath = shellQuote(`${authorizedKeysPath}.paratix-backup`);
|
|
13066
13263
|
const replace2 = await conn.exec(
|
|
13067
|
-
`chmod 600 ${quotedTemporaryPath} && chown ${shellQuote(user2)}:${shellQuote(primaryGroup)} ${quotedTemporaryPath} && {
|
|
13264
|
+
`chmod 600 ${quotedTemporaryPath} && chown ${shellQuote(user2)}:${shellQuote(primaryGroup)} ${quotedTemporaryPath} && {
|
|
13265
|
+
expected_authorized_keys_hash=$(sha256sum ${quotedTemporaryPath} | cut -d' ' -f1) || exit $?
|
|
13266
|
+
[ ! -L ${quotedSshDirectoryPath} ] || { echo '.ssh must not be a symlink' >&2; exit 1; }
|
|
13267
|
+
[ -d ${quotedSshDirectoryPath} ] || { echo '.ssh must be a directory' >&2; exit 1; }
|
|
13268
|
+
ssh_directory_state=$(stat -c '%a %U %G %F' ${quotedSshDirectoryPath}) || exit $?
|
|
13269
|
+
[ "$ssh_directory_state" = ${expectedSshDirectoryState} ] || { echo '.ssh ownership changed before authorized_keys replace' >&2; exit 1; }
|
|
13270
|
+
[ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13271
|
+
rm -f -- ${quotedBackupPath}
|
|
13272
|
+
backup_created=0
|
|
13273
|
+
if [ -e ${quotedAuthorizedKeysPath} ]; then
|
|
13274
|
+
[ -f ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must be a regular file' >&2; exit 1; }
|
|
13275
|
+
[ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13276
|
+
ln -P -- ${quotedAuthorizedKeysPath} ${quotedBackupPath} || { echo 'failed to create authorized_keys backup hardlink' >&2; exit 1; }
|
|
13277
|
+
backup_created=1
|
|
13278
|
+
fi
|
|
13279
|
+
[ ! -L ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13280
|
+
if ! mv -T -- ${quotedTemporaryPath} ${quotedAuthorizedKeysPath}; then
|
|
13281
|
+
if [ "$backup_created" = 1 ]; then
|
|
13282
|
+
mv -T -- ${quotedBackupPath} ${quotedAuthorizedKeysPath} || echo 'authorized_keys backup restore failed; backup is at '${quotedBackupPath} >&2
|
|
13283
|
+
fi
|
|
13284
|
+
echo 'authorized_keys replace failed' >&2
|
|
13285
|
+
exit 1
|
|
13286
|
+
fi
|
|
13287
|
+
[ ! -e ${quotedTemporaryPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys replace did not consume temporary file' >&2; exit 1; }
|
|
13288
|
+
[ ! -L ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must not be a symlink' >&2; exit 1; }
|
|
13289
|
+
[ -f ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must be a regular file' >&2; exit 1; }
|
|
13290
|
+
authorized_keys_state=$(stat -c '%a %U %G %F' ${quotedAuthorizedKeysPath}) || { rm -f -- ${quotedBackupPath}; exit 1; }
|
|
13291
|
+
[ "$authorized_keys_state" = ${expectedAuthorizedKeysState} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys metadata changed during replace' >&2; exit 1; }
|
|
13292
|
+
authorized_keys_hash=$(sha256sum ${quotedAuthorizedKeysPath} | cut -d' ' -f1) || { rm -f -- ${quotedBackupPath}; exit 1; }
|
|
13293
|
+
[ "$authorized_keys_hash" = "$expected_authorized_keys_hash" ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys content changed during replace' >&2; exit 1; }
|
|
13294
|
+
rm -f -- ${quotedBackupPath}
|
|
13295
|
+
}`,
|
|
13068
13296
|
MUTATION_EXEC_OPTS
|
|
13069
13297
|
);
|
|
13070
13298
|
if (replace2.code !== 0) {
|
|
@@ -19068,4 +19296,3 @@ export {
|
|
|
19068
19296
|
ufw,
|
|
19069
19297
|
user
|
|
19070
19298
|
};
|
|
19071
|
-
//# sourceMappingURL=chunk-CWGQ2MYN.js.map
|