paratix 0.12.8 → 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-M3ZQJNKM.js → chunk-XIRORNO3.js} +249 -24
- package/dist/cli.js +2250 -1529
- package/dist/{index-udpAybq3.d.ts → index-DwKdTyHo.d.ts} +25 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.js +7 -8
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +1 -2
- package/llm-guide.md +17 -17
- 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
|
@@ -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).
|
|
@@ -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
|
}
|
|
@@ -3323,10 +3316,12 @@ var EXEC_OPTS4 = { ignoreExitCode: true, silent: true };
|
|
|
3323
3316
|
var UNIT_NAME_PATTERN = new RegExp("^[\\w@.\\-]+$", "v");
|
|
3324
3317
|
var COMPOSE_CONFIG_MODE = "0600";
|
|
3325
3318
|
var COMPOSE_CONFIG_STAGING_PREFIX = ".compose.yml.paratix-staging";
|
|
3319
|
+
var COMPOSE_IMAGE_INSPECT_FORMAT = "{{.Id}}\\n{{range .RepoDigests}}{{.}}\\n{{end}}";
|
|
3326
3320
|
var SYSTEMD_UNIT_MODE = "0644";
|
|
3327
3321
|
var SYSTEMD_UNIT_STAGING_PREFIX = ".compose-systemd-unit.paratix-staging";
|
|
3328
3322
|
var C0_CONTROL_MAX_CODE_POINT = 31;
|
|
3329
3323
|
var DELETE_CONTROL_CODE_POINT2 = 127;
|
|
3324
|
+
var COMPOSE_CONFIG_JSON_MAX_BYTES = 1048576;
|
|
3330
3325
|
function requireComposeSsh(ssh2, action, projectDirectory) {
|
|
3331
3326
|
return ssh2 ?? failed(`[compose.${action}] SSH connection is required for ${projectDirectory}`);
|
|
3332
3327
|
}
|
|
@@ -3351,10 +3346,6 @@ var COMPOSE_UP_ACTION_KEYWORDS_REGEX = new RegExp("^(?:Creating|Recreating|Start
|
|
|
3351
3346
|
function composeUpReportedChange(composeOutput) {
|
|
3352
3347
|
return COMPOSE_UP_ACTION_KEYWORDS_REGEX.test(composeOutput);
|
|
3353
3348
|
}
|
|
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
3349
|
function validateComposeUpServices(services) {
|
|
3359
3350
|
for (const service2 of services ?? []) {
|
|
3360
3351
|
if (service2 === "") {
|
|
@@ -3392,6 +3383,148 @@ function parseComposeProjectName(stdout, projectDirectory) {
|
|
|
3392
3383
|
return null;
|
|
3393
3384
|
}
|
|
3394
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
|
+
}
|
|
3395
3528
|
async function resolveComposeProjectName(parameters) {
|
|
3396
3529
|
const result = await parameters.ssh.exec(
|
|
3397
3530
|
`${composeCommand(parameters.runtime, parameters.projectDirectory)} config --format json`,
|
|
@@ -4050,13 +4183,33 @@ var compose = {
|
|
|
4050
4183
|
ssh: connection
|
|
4051
4184
|
});
|
|
4052
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
|
+
});
|
|
4053
4197
|
const result = await connection.exec(
|
|
4054
4198
|
`${composeCommand(runtime, projectDirectory)} pull 2>&1`,
|
|
4055
4199
|
EXEC_OPTS4
|
|
4056
4200
|
);
|
|
4057
4201
|
if (result.code !== 0)
|
|
4058
4202
|
return failedCommand(`[compose.pull] failed for ${projectDirectory}`, result);
|
|
4059
|
-
|
|
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
|
+
};
|
|
4060
4213
|
},
|
|
4061
4214
|
// eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
|
|
4062
4215
|
async check() {
|
|
@@ -6185,7 +6338,7 @@ function describeType(value) {
|
|
|
6185
6338
|
function isHostKeyMode(value) {
|
|
6186
6339
|
return value === "accept-new" || value === "no" || value === "yes";
|
|
6187
6340
|
}
|
|
6188
|
-
function
|
|
6341
|
+
function isRecord2(value) {
|
|
6189
6342
|
return Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null;
|
|
6190
6343
|
}
|
|
6191
6344
|
function collectOptionalBooleanErrors(object, key, errors) {
|
|
@@ -6304,7 +6457,7 @@ function collectSshConfigErrors(value) {
|
|
|
6304
6457
|
errors.push(`Invalid property 'ssh' (expected object, got ${describeType(value)})`);
|
|
6305
6458
|
return errors;
|
|
6306
6459
|
}
|
|
6307
|
-
if (!
|
|
6460
|
+
if (!isRecord2(value)) {
|
|
6308
6461
|
errors.push("Invalid property 'ssh' (expected object, got object)");
|
|
6309
6462
|
return errors;
|
|
6310
6463
|
}
|
|
@@ -6329,7 +6482,8 @@ function normalizeServerDefinitionSshError(error) {
|
|
|
6329
6482
|
function validateSshConfig(ssh2) {
|
|
6330
6483
|
const errors = collectSshConfigErrors(ssh2);
|
|
6331
6484
|
if (errors.length === 0) return;
|
|
6332
|
-
|
|
6485
|
+
const message = errors.map((error) => normalizeServerDefinitionSshError(error)).join("; ");
|
|
6486
|
+
throw new Error(`ServerDefinition: ${message}`);
|
|
6333
6487
|
}
|
|
6334
6488
|
|
|
6335
6489
|
// src/meta.ts
|
|
@@ -6372,7 +6526,7 @@ function normalizeMetaValueResolver(value) {
|
|
|
6372
6526
|
return value;
|
|
6373
6527
|
};
|
|
6374
6528
|
}
|
|
6375
|
-
function
|
|
6529
|
+
function isRecord3(value) {
|
|
6376
6530
|
return typeof value === "object" && value !== null;
|
|
6377
6531
|
}
|
|
6378
6532
|
function assertValidEnvironmentMetaEntry(candidate) {
|
|
@@ -6460,7 +6614,7 @@ function isSystemRebootMetaEntry(entry) {
|
|
|
6460
6614
|
return entry.kind === SYSTEM_REBOOT_KIND;
|
|
6461
6615
|
}
|
|
6462
6616
|
function assertValidModuleMetaEntry(entry) {
|
|
6463
|
-
if (!
|
|
6617
|
+
if (!isRecord3(entry)) {
|
|
6464
6618
|
throw new TypeError(`Invalid meta entry: expected object, got ${typeof entry}`);
|
|
6465
6619
|
}
|
|
6466
6620
|
switch (entry.kind) {
|
|
@@ -13000,7 +13154,20 @@ async function ensureSshDirectoryAndAuthorizedKeysAreNotSymlinks(conn, parameter
|
|
|
13000
13154
|
const keysPath = shellQuote(authorizedKeysPath);
|
|
13001
13155
|
const quotedUser = shellQuote(user2);
|
|
13002
13156
|
const quotedPrimaryGroup = shellQuote(primaryGroup);
|
|
13003
|
-
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; }`;
|
|
13004
13171
|
const result = await conn.exec(command2, MUTATION_EXEC_OPTS);
|
|
13005
13172
|
if (result.code !== 0) {
|
|
13006
13173
|
const label = result.stderr.includes("authorized_keys must not be a symlink") ? "authorized_keys symlink check failed" : "failed to prepare .ssh directory";
|
|
@@ -13030,10 +13197,25 @@ async function stageAuthorizedKeysContent(conn, parameters) {
|
|
|
13030
13197
|
const filterScratchPath = `${temporaryPath}.filter`;
|
|
13031
13198
|
const quotedFilterScratchPath = shellQuote(filterScratchPath);
|
|
13032
13199
|
const quotedKey = shellQuote(key);
|
|
13033
|
-
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; }`;
|
|
13034
13202
|
const readExistingAuthorizedKeys = `dd if=${quotedAuthorizedKeysPath} iflag=nofollow status=none of=${quotedTemporaryPath}`;
|
|
13035
13203
|
const stage = state === "present" ? await conn.exec(
|
|
13036
|
-
`{ 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; }`,
|
|
13037
13219
|
MUTATION_EXEC_OPTS
|
|
13038
13220
|
) : (
|
|
13039
13221
|
// R-0000044: use `grep -vxF` (whole-line match) to mirror the present
|
|
@@ -13045,7 +13227,20 @@ async function stageAuthorizedKeysContent(conn, parameters) {
|
|
|
13045
13227
|
// TOCTOU where the path could become a symlink between the
|
|
13046
13228
|
// `[ ! -L ]` guard and grep's open() call.
|
|
13047
13229
|
await conn.exec(
|
|
13048
|
-
`{ 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; }`,
|
|
13049
13244
|
MUTATION_EXEC_OPTS
|
|
13050
13245
|
)
|
|
13051
13246
|
);
|
|
@@ -13066,7 +13261,38 @@ async function replaceAuthorizedKeysAtomically(conn, parameters) {
|
|
|
13066
13261
|
const expectedAuthorizedKeysState = shellQuote(`600 ${user2} ${primaryGroup} regular file`);
|
|
13067
13262
|
const quotedBackupPath = shellQuote(`${authorizedKeysPath}.paratix-backup`);
|
|
13068
13263
|
const replace2 = await conn.exec(
|
|
13069
|
-
`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
|
+
}`,
|
|
13070
13296
|
MUTATION_EXEC_OPTS
|
|
13071
13297
|
);
|
|
13072
13298
|
if (replace2.code !== 0) {
|
|
@@ -19070,4 +19296,3 @@ export {
|
|
|
19070
19296
|
ufw,
|
|
19071
19297
|
user
|
|
19072
19298
|
};
|
|
19073
|
-
//# sourceMappingURL=chunk-M3ZQJNKM.js.map
|