@matthesketh/fleet 1.8.1 → 1.11.1
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 +186 -16
- package/dist/bin/fleet-agent.d.ts +2 -0
- package/dist/bin/fleet-agent.js +7 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +73 -31
- package/dist/commands/add.d.ts +2 -1
- package/dist/commands/add.js +66 -59
- package/dist/commands/audit.d.ts +1 -0
- package/dist/commands/audit.js +144 -0
- package/dist/commands/backup.d.ts +1 -0
- package/dist/commands/backup.js +510 -0
- package/dist/commands/boot-start.d.ts +3 -1
- package/dist/commands/boot-start.js +39 -47
- package/dist/commands/completions.d.ts +6 -0
- package/dist/commands/completions.js +83 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.js +96 -0
- package/dist/commands/deploy.js +3 -2
- package/dist/commands/deps.js +5 -1
- package/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.js +186 -0
- package/dist/commands/egress.d.ts +1 -1
- package/dist/commands/egress.js +13 -10
- package/dist/commands/freeze.d.ts +8 -4
- package/dist/commands/freeze.js +77 -59
- package/dist/commands/git.js +2 -2
- package/dist/commands/health.d.ts +2 -1
- package/dist/commands/health.js +38 -56
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +83 -73
- package/dist/commands/install-mcp.d.ts +3 -1
- package/dist/commands/install-mcp.js +53 -34
- package/dist/commands/list.d.ts +2 -1
- package/dist/commands/list.js +22 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/patch-systemd.d.ts +7 -1
- package/dist/commands/patch-systemd.js +71 -31
- package/dist/commands/remove.d.ts +3 -1
- package/dist/commands/remove.js +37 -26
- package/dist/commands/restart.d.ts +4 -1
- package/dist/commands/restart.js +17 -20
- package/dist/commands/rollback.d.ts +4 -1
- package/dist/commands/rollback.js +33 -42
- package/dist/commands/secrets.js +157 -9
- package/dist/commands/start.d.ts +4 -1
- package/dist/commands/start.js +17 -20
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +21 -26
- package/dist/commands/stop.d.ts +4 -1
- package/dist/commands/stop.js +17 -20
- package/dist/commands/testflight.d.ts +1 -0
- package/dist/commands/testflight.js +193 -0
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.js +95 -0
- package/dist/core/audit/cache.d.ts +4 -0
- package/dist/core/audit/cache.js +37 -0
- package/dist/core/audit/config.d.ts +5 -0
- package/dist/core/audit/config.js +35 -0
- package/dist/core/audit/greenlight.d.ts +11 -0
- package/dist/core/audit/greenlight.js +81 -0
- package/dist/core/audit/reporters/cli.d.ts +3 -0
- package/dist/core/audit/reporters/cli.js +68 -0
- package/dist/core/audit/suppress.d.ts +6 -0
- package/dist/core/audit/suppress.js +37 -0
- package/dist/core/audit/target.d.ts +5 -0
- package/dist/core/audit/target.js +26 -0
- package/dist/core/audit/types.d.ts +54 -0
- package/dist/core/audit/types.js +5 -0
- package/dist/core/backup/browser-api.d.ts +66 -0
- package/dist/core/backup/browser-api.js +197 -0
- package/dist/core/backup/browser-server.d.ts +11 -0
- package/dist/core/backup/browser-server.js +241 -0
- package/dist/core/backup/browser-ui.d.ts +5 -0
- package/dist/core/backup/browser-ui.js +268 -0
- package/dist/core/backup/cloudflare.d.ts +7 -0
- package/dist/core/backup/cloudflare.js +82 -0
- package/dist/core/backup/config.d.ts +9 -0
- package/dist/core/backup/config.js +80 -0
- package/dist/core/backup/detect.d.ts +11 -0
- package/dist/core/backup/detect.js +71 -0
- package/dist/core/backup/dump.d.ts +11 -0
- package/dist/core/backup/dump.js +82 -0
- package/dist/core/backup/index.d.ts +9 -0
- package/dist/core/backup/index.js +9 -0
- package/dist/core/backup/repo.d.ts +71 -0
- package/dist/core/backup/repo.js +256 -0
- package/dist/core/backup/schedule.d.ts +17 -0
- package/dist/core/backup/schedule.js +90 -0
- package/dist/core/backup/sensitive.d.ts +5 -0
- package/dist/core/backup/sensitive.js +37 -0
- package/dist/core/backup/status.d.ts +3 -0
- package/dist/core/backup/status.js +29 -0
- package/dist/core/backup/statuspage.d.ts +23 -0
- package/dist/core/backup/statuspage.js +145 -0
- package/dist/core/backup/system.d.ts +24 -0
- package/dist/core/backup/system.js +209 -0
- package/dist/core/backup/totp.d.ts +16 -0
- package/dist/core/backup/totp.js +116 -0
- package/dist/core/backup/types.d.ts +70 -0
- package/dist/core/backup/types.js +7 -0
- package/dist/core/backup/unlock.d.ts +19 -0
- package/dist/core/backup/unlock.js +69 -0
- package/dist/core/boot-refresh.d.ts +1 -1
- package/dist/core/boot-refresh.js +10 -9
- package/dist/core/deps/actors/pr-creator.d.ts +5 -3
- package/dist/core/deps/actors/pr-creator.js +71 -18
- package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
- package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
- package/dist/core/deps/collectors/npm.js +3 -1
- package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
- package/dist/core/deps/collectors/vulnerability.js +31 -2
- package/dist/core/deps/config.js +6 -0
- package/dist/core/deps/scanner.js +1 -1
- package/dist/core/deps/types.d.ts +8 -0
- package/dist/core/env.d.ts +3 -0
- package/dist/core/env.js +11 -0
- package/dist/core/exec.d.ts +1 -0
- package/dist/core/exec.js +4 -0
- package/dist/core/file-lock.d.ts +18 -0
- package/dist/core/file-lock.js +44 -0
- package/dist/core/git-onboard.js +10 -13
- package/dist/core/github.d.ts +3 -1
- package/dist/core/github.js +10 -7
- package/dist/core/logs-policy.d.ts +5 -0
- package/dist/core/logs-policy.js +20 -1
- package/dist/core/operator.d.ts +21 -0
- package/dist/core/operator.js +54 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.js +26 -0
- package/dist/core/routines/schema.d.ts +11 -11
- package/dist/core/routines/schema.js +14 -3
- package/dist/core/routines/store.d.ts +8 -8
- package/dist/core/secrets-ops.d.ts +31 -6
- package/dist/core/secrets-ops.js +208 -102
- package/dist/core/secrets-providers.js +2 -2
- package/dist/core/secrets-rotation.d.ts +1 -1
- package/dist/core/secrets-rotation.js +58 -52
- package/dist/core/secrets-v2-cleanup.d.ts +19 -0
- package/dist/core/secrets-v2-cleanup.js +94 -0
- package/dist/core/secrets-v2-creds.d.ts +9 -0
- package/dist/core/secrets-v2-creds.js +44 -0
- package/dist/core/secrets-v2-install.d.ts +13 -0
- package/dist/core/secrets-v2-install.js +76 -0
- package/dist/core/secrets-v2-keypair.d.ts +10 -0
- package/dist/core/secrets-v2-keypair.js +31 -0
- package/dist/core/secrets-v2-migrate.d.ts +29 -0
- package/dist/core/secrets-v2-migrate.js +395 -0
- package/dist/core/secrets-v2-ops.d.ts +36 -0
- package/dist/core/secrets-v2-ops.js +184 -0
- package/dist/core/secrets-v2-protocol.d.ts +19 -0
- package/dist/core/secrets-v2-protocol.js +60 -0
- package/dist/core/secrets-v2-snapshot.d.ts +36 -0
- package/dist/core/secrets-v2-snapshot.js +115 -0
- package/dist/core/secrets-v2.d.ts +21 -0
- package/dist/core/secrets-v2.js +249 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +91 -11
- package/dist/core/self-update.d.ts +32 -11
- package/dist/core/self-update.js +52 -14
- package/dist/core/testflight/asc.d.ts +12 -0
- package/dist/core/testflight/asc.js +101 -0
- package/dist/core/testflight/credentials.d.ts +3 -0
- package/dist/core/testflight/credentials.js +35 -0
- package/dist/core/testflight/resolve.d.ts +6 -0
- package/dist/core/testflight/resolve.js +44 -0
- package/dist/core/testflight/types.d.ts +13 -0
- package/dist/core/testflight/types.js +3 -0
- package/dist/core/testflight/workflow.d.ts +17 -0
- package/dist/core/testflight/workflow.js +65 -0
- package/dist/core/validate.d.ts +1 -0
- package/dist/core/validate.js +8 -0
- package/dist/index.js +0 -0
- package/dist/mcp/audit-tools.d.ts +2 -0
- package/dist/mcp/audit-tools.js +94 -0
- package/dist/mcp/git-tools.js +1 -1
- package/dist/mcp/registry-bridge.d.ts +10 -0
- package/dist/mcp/registry-bridge.js +65 -0
- package/dist/mcp/secrets-tools.js +2 -2
- package/dist/mcp/server.js +16 -82
- package/dist/mcp/testflight-tools.d.ts +2 -0
- package/dist/mcp/testflight-tools.js +52 -0
- package/dist/registry/context.d.ts +7 -0
- package/dist/registry/context.js +37 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +44 -0
- package/dist/registry/parse-args.d.ts +13 -0
- package/dist/registry/parse-args.js +74 -0
- package/dist/registry/registry.d.ts +24 -0
- package/dist/registry/registry.js +26 -0
- package/dist/registry/render.d.ts +3 -0
- package/dist/registry/render.js +29 -0
- package/dist/registry/types.d.ts +50 -0
- package/dist/registry/types.js +1 -0
- package/dist/templates/agent-unit.d.ts +5 -0
- package/dist/templates/agent-unit.js +40 -0
- package/dist/templates/app-unit-edit.d.ts +2 -0
- package/dist/templates/app-unit-edit.js +46 -0
- package/dist/templates/compose-edit.d.ts +2 -0
- package/dist/templates/compose-edit.js +156 -0
- package/dist/templates/nginx.js +11 -0
- package/dist/templates/systemd.js +6 -0
- package/dist/tui/components/ArgForm.d.ts +7 -0
- package/dist/tui/components/ArgForm.js +64 -0
- package/dist/tui/components/ArgForm.test.d.ts +1 -0
- package/dist/tui/components/ArgForm.test.js +19 -0
- package/dist/tui/components/KeyHint.js +5 -0
- package/dist/tui/hooks/use-secrets.d.ts +8 -8
- package/dist/tui/hooks/use-secrets.js +7 -7
- package/dist/tui/router.d.ts +1 -0
- package/dist/tui/router.js +26 -9
- package/dist/tui/router.test.d.ts +1 -0
- package/dist/tui/router.test.js +13 -0
- package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
- package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
- package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
- package/dist/tui/tests/redaction-rerender.test.js +53 -0
- package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
- package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
- package/dist/tui/types.d.ts +1 -1
- package/dist/tui/views/CommandPalette.d.ts +5 -0
- package/dist/tui/views/CommandPalette.js +90 -0
- package/dist/tui/views/CommandPalette.test.d.ts +1 -0
- package/dist/tui/views/CommandPalette.test.js +117 -0
- package/dist/tui/views/Dashboard.js +9 -6
- package/dist/tui/views/HealthView.js +9 -4
- package/dist/tui/views/SecretEdit.js +15 -16
- package/dist/tui/views/SecretEdit.test.d.ts +1 -0
- package/dist/tui/views/SecretEdit.test.js +82 -0
- package/dist/tui/views/SecretsView.js +26 -16
- package/package.json +8 -5
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import { load } from '../core/registry.js';
|
|
3
4
|
import { readServiceFile } from '../core/systemd.js';
|
|
4
5
|
import { execSafe } from '../core/exec.js';
|
|
5
|
-
import {
|
|
6
|
+
import { defineCommand } from '../registry/registry.js';
|
|
6
7
|
const SERVICE_DIR = '/etc/systemd/system';
|
|
7
|
-
|
|
8
|
-
if (args.includes('--rollback'))
|
|
9
|
-
return rollback();
|
|
8
|
+
function runPatch(ctx) {
|
|
10
9
|
const reg = load();
|
|
11
10
|
const dbServiceName = reg.infrastructure.databases.serviceName;
|
|
12
11
|
const appServiceNames = reg.apps.map(a => a.serviceName);
|
|
@@ -20,20 +19,20 @@ export function patchSystemdCommand(args) {
|
|
|
20
19
|
}
|
|
21
20
|
targetMap.set(dbServiceName, { name: dbServiceName, rewriteExecStart: false });
|
|
22
21
|
const targets = Array.from(targetMap.values());
|
|
23
|
-
info
|
|
22
|
+
ctx.log({ level: 'info', message: `patching ${targets.length} service(s)...` });
|
|
24
23
|
let patched = 0;
|
|
25
24
|
let skipped = 0;
|
|
26
25
|
for (const { name, rewriteExecStart } of targets) {
|
|
27
26
|
const path = `${SERVICE_DIR}/${name}.service`;
|
|
28
27
|
const content = readServiceFile(name);
|
|
29
28
|
if (content === null) {
|
|
30
|
-
warn
|
|
29
|
+
ctx.log({ level: 'warn', message: `${name}: no service file found, skipping` });
|
|
31
30
|
skipped++;
|
|
32
31
|
continue;
|
|
33
32
|
}
|
|
34
33
|
let updated = content;
|
|
35
34
|
let changed = false;
|
|
36
|
-
//
|
|
35
|
+
// existing behaviour: add StartLimitBurst if missing (applies to ALL services including databases)
|
|
37
36
|
if (!updated.includes('StartLimitBurst=')) {
|
|
38
37
|
updated = updated.replace(/(\[Service\])/, '$1\nStartLimitBurst=5\nStartLimitIntervalSec=300');
|
|
39
38
|
changed = true;
|
|
@@ -45,7 +44,7 @@ export function patchSystemdCommand(args) {
|
|
|
45
44
|
updated = updated.replace(/^ExecStart=.*$/m, expectedExecStart);
|
|
46
45
|
changed = true;
|
|
47
46
|
}
|
|
48
|
-
//
|
|
47
|
+
// ensure TimeoutStartSec=900
|
|
49
48
|
if (!updated.includes('TimeoutStartSec=900')) {
|
|
50
49
|
if (/^TimeoutStartSec=\d+/m.test(updated)) {
|
|
51
50
|
updated = updated.replace(/^TimeoutStartSec=\d+.*$/m, 'TimeoutStartSec=900');
|
|
@@ -57,37 +56,49 @@ export function patchSystemdCommand(args) {
|
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
58
|
if (!changed) {
|
|
60
|
-
info
|
|
59
|
+
ctx.log({ level: 'info', message: `${name}: already patched, skipping` });
|
|
61
60
|
skipped++;
|
|
62
61
|
continue;
|
|
63
62
|
}
|
|
64
|
-
//
|
|
63
|
+
// backup original before overwrite
|
|
65
64
|
try {
|
|
66
65
|
copyFileSync(path, `${path}.bak`);
|
|
67
66
|
}
|
|
68
67
|
catch (err) {
|
|
69
|
-
|
|
68
|
+
ctx.log({
|
|
69
|
+
level: 'warn',
|
|
70
|
+
message: `${name}: failed to create .bak (${err instanceof Error ? err.message : String(err)}); skipping for safety`,
|
|
71
|
+
});
|
|
70
72
|
skipped++;
|
|
71
73
|
continue;
|
|
72
74
|
}
|
|
73
75
|
writeFileSync(path, updated);
|
|
74
|
-
|
|
76
|
+
ctx.log({ level: 'info', message: `${name}: patched` });
|
|
75
77
|
patched++;
|
|
76
78
|
}
|
|
77
79
|
if (patched === 0) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
summary: 'no services needed patching',
|
|
83
|
+
data: { action: 'patch', changed: 0, skipped },
|
|
84
|
+
};
|
|
80
85
|
}
|
|
81
|
-
|
|
86
|
+
ctx.log({ level: 'info', message: 'running systemctl daemon-reload...' });
|
|
82
87
|
const result = execSafe('systemctl', ['daemon-reload']);
|
|
83
|
-
if (result.ok) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
if (!result.ok) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
summary: `patched ${patched} service(s) but daemon-reload failed: ${result.stderr}`,
|
|
92
|
+
data: { action: 'patch', changed: patched, skipped },
|
|
93
|
+
};
|
|
88
94
|
}
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
summary: `patched ${patched} service(s), skipped ${skipped}`,
|
|
98
|
+
data: { action: 'patch', changed: patched, skipped },
|
|
99
|
+
};
|
|
89
100
|
}
|
|
90
|
-
function
|
|
101
|
+
function runRollback(ctx) {
|
|
91
102
|
const reg = load();
|
|
92
103
|
const serviceNames = [
|
|
93
104
|
...reg.apps.map(a => a.serviceName),
|
|
@@ -104,23 +115,52 @@ function rollback() {
|
|
|
104
115
|
}
|
|
105
116
|
try {
|
|
106
117
|
renameSync(bak, path);
|
|
107
|
-
|
|
118
|
+
ctx.log({ level: 'info', message: `${name}: restored from .bak` });
|
|
108
119
|
restored++;
|
|
109
120
|
}
|
|
110
121
|
catch (err) {
|
|
111
|
-
|
|
122
|
+
ctx.log({
|
|
123
|
+
level: 'error',
|
|
124
|
+
message: `${name}: failed to restore: ${err instanceof Error ? err.message : String(err)}`,
|
|
125
|
+
});
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
if (restored === 0) {
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
summary: 'no .bak files found to restore',
|
|
132
|
+
data: { action: 'rollback', changed: 0, skipped: missing },
|
|
133
|
+
};
|
|
117
134
|
}
|
|
118
|
-
|
|
135
|
+
ctx.log({ level: 'info', message: 'running systemctl daemon-reload...' });
|
|
119
136
|
const result = execSafe('systemctl', ['daemon-reload']);
|
|
120
|
-
if (result.ok) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
summary: `restored ${restored} but daemon-reload failed: ${result.stderr}`,
|
|
141
|
+
data: { action: 'rollback', changed: restored, skipped: missing },
|
|
142
|
+
};
|
|
125
143
|
}
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
summary: `restored ${restored}, missing ${missing}`,
|
|
147
|
+
data: { action: 'rollback', changed: restored, skipped: missing },
|
|
148
|
+
};
|
|
126
149
|
}
|
|
150
|
+
export const patchSystemdCommand = defineCommand({
|
|
151
|
+
name: 'patch-systemd',
|
|
152
|
+
summary: 'Add StartLimit settings to all service files',
|
|
153
|
+
args: z.object({ rollback: z.boolean().default(false), yes: z.boolean().default(false) }),
|
|
154
|
+
destructive: true,
|
|
155
|
+
async run(args, ctx) {
|
|
156
|
+
const verb = args.rollback ? 'roll back' : 'patch';
|
|
157
|
+
if (!args.yes && !(await ctx.confirm(`${verb} all fleet systemd unit files?`))) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
summary: 'cancelled',
|
|
161
|
+
data: { action: args.rollback ? 'rollback' : 'patch', changed: 0, skipped: 0 },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return args.rollback ? runRollback(ctx) : runPatch(ctx);
|
|
165
|
+
},
|
|
166
|
+
});
|
package/dist/commands/remove.js
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { load, findApp, removeApp, withRegistry } from '../core/registry.js';
|
|
2
3
|
import { stopService, disableService } from '../core/systemd.js';
|
|
3
4
|
import { AppNotFoundError } from '../core/errors.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
5
|
+
import { defineCommand } from '../registry/registry.js';
|
|
6
|
+
export const removeCommand = defineCommand({
|
|
7
|
+
name: 'remove',
|
|
8
|
+
summary: 'Stop, disable and deregister an app',
|
|
9
|
+
args: z.object({ app: z.string(), yes: z.boolean().default(false) }),
|
|
10
|
+
destructive: true,
|
|
11
|
+
async run(args, ctx) {
|
|
12
|
+
const app = findApp(load(), args.app);
|
|
13
|
+
if (!app) {
|
|
14
|
+
return { ok: false, summary: `app not found: ${args.app}`, data: { app: args.app } };
|
|
15
|
+
}
|
|
16
|
+
if (!args.yes && !(await ctx.confirm(`Remove ${app.name}? This will stop and disable the service.`))) {
|
|
17
|
+
return { ok: false, summary: 'cancelled', data: { app: app.name } };
|
|
18
|
+
}
|
|
19
|
+
// systemctl runs outside the registry lock so we don't hold it while
|
|
20
|
+
// services stop/disable.
|
|
21
|
+
stopService(app.serviceName);
|
|
22
|
+
disableService(app.serviceName);
|
|
23
|
+
try {
|
|
24
|
+
await withRegistry(reg => {
|
|
25
|
+
const fresh = findApp(reg, app.name);
|
|
26
|
+
if (!fresh)
|
|
27
|
+
throw new AppNotFoundError(app.name);
|
|
28
|
+
return removeApp(reg, fresh.name);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
// a concurrent process removed the app between the unlocked preview and
|
|
33
|
+
// the locked mutation — surface it as a graceful expected failure.
|
|
34
|
+
return { ok: false, summary: err instanceof Error ? err.message : String(err), data: { app: app.name } };
|
|
35
|
+
}
|
|
36
|
+
ctx.log({ level: 'warn', message: 'service file not deleted — remove manually if needed' });
|
|
37
|
+
return { ok: true, summary: `removed ${app.name} from registry`, data: { app: app.name } };
|
|
38
|
+
},
|
|
39
|
+
});
|
package/dist/commands/restart.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { load, findApp } from '../core/registry.js';
|
|
2
3
|
import { restartService } from '../core/systemd.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
error(`Failed to restart ${app.name}`);
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
4
|
+
import { defineCommand } from '../registry/registry.js';
|
|
5
|
+
export const restartCommand = defineCommand({
|
|
6
|
+
name: 'restart',
|
|
7
|
+
summary: 'Restart an app via systemctl',
|
|
8
|
+
args: z.object({ app: z.string() }),
|
|
9
|
+
async run(args) {
|
|
10
|
+
const app = findApp(load(), args.app);
|
|
11
|
+
if (!app) {
|
|
12
|
+
return { ok: false, summary: `app not found: ${args.app}`, data: { app: args.app, service: '' } };
|
|
13
|
+
}
|
|
14
|
+
if (!restartService(app.serviceName)) {
|
|
15
|
+
return { ok: false, summary: `failed to restart ${app.name}`, data: { app: app.name, service: app.serviceName } };
|
|
16
|
+
}
|
|
17
|
+
return { ok: true, summary: `restarted ${app.name}`, data: { app: app.name, service: app.serviceName } };
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { load, findApp } from '../core/registry.js';
|
|
2
3
|
import { execSafe } from '../core/exec.js';
|
|
3
4
|
import { restartService } from '../core/systemd.js';
|
|
4
|
-
|
|
5
|
-
process.stdout.write(`[rollback] ${msg}\n`);
|
|
6
|
-
}
|
|
7
|
-
function logErr(msg) {
|
|
8
|
-
process.stderr.write(`[rollback] ${msg}\n`);
|
|
9
|
-
}
|
|
5
|
+
import { defineCommand } from '../registry/registry.js';
|
|
10
6
|
function resolveImageName(composePath, composeFile) {
|
|
11
7
|
const args = ['compose', ...(composeFile ? ['-f', composeFile] : []), 'config', '--images'];
|
|
12
8
|
const r = execSafe('docker', args, { cwd: composePath, timeout: 15_000 });
|
|
@@ -20,39 +16,34 @@ function splitImageBase(image) {
|
|
|
20
16
|
return image;
|
|
21
17
|
return image.slice(0, lastColon);
|
|
22
18
|
}
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
logErr(`tag restored but service restart failed for ${app.serviceName}`);
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
log(`rolled back ${app.name} to ${previous}`);
|
|
58
|
-
}
|
|
19
|
+
export const rollbackCommand = defineCommand({
|
|
20
|
+
name: 'rollback',
|
|
21
|
+
summary: 'Roll back app to previous image',
|
|
22
|
+
args: z.object({ app: z.string(), yes: z.boolean().default(false) }),
|
|
23
|
+
destructive: true,
|
|
24
|
+
async run(args, ctx) {
|
|
25
|
+
const app = findApp(load(), args.app);
|
|
26
|
+
if (!app) {
|
|
27
|
+
return { ok: false, summary: `app not found: ${args.app}`, data: { app: args.app, image: '' } };
|
|
28
|
+
}
|
|
29
|
+
const image = resolveImageName(app.composePath, app.composeFile);
|
|
30
|
+
if (!image) {
|
|
31
|
+
return { ok: false, summary: `could not resolve image name for ${app.name}`, data: { app: app.name, image: '' } };
|
|
32
|
+
}
|
|
33
|
+
const previous = `${splitImageBase(image)}:fleet-previous`;
|
|
34
|
+
if (!execSafe('docker', ['image', 'inspect', previous], { timeout: 10_000 }).ok) {
|
|
35
|
+
return { ok: false, summary: `no previous image found (${previous}) — nothing to roll back to`, data: { app: app.name, image: '' } };
|
|
36
|
+
}
|
|
37
|
+
if (!(args.yes || (await ctx.confirm(`Roll back ${app.name} to ${previous} and restart?`)))) {
|
|
38
|
+
return { ok: false, summary: 'cancelled', data: { app: app.name, image: previous } };
|
|
39
|
+
}
|
|
40
|
+
const tag = execSafe('docker', ['tag', previous, image], { timeout: 10_000 });
|
|
41
|
+
if (!tag.ok) {
|
|
42
|
+
return { ok: false, summary: `docker tag failed: ${tag.stderr || `exit ${tag.exitCode}`}`, data: { app: app.name, image: previous } };
|
|
43
|
+
}
|
|
44
|
+
if (!restartService(app.serviceName)) {
|
|
45
|
+
return { ok: false, summary: `tag restored but service restart failed for ${app.serviceName}`, data: { app: app.name, image: previous } };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, summary: `rolled back ${app.name} to ${previous}`, data: { app: app.name, image: previous } };
|
|
48
|
+
},
|
|
49
|
+
});
|
package/dist/commands/secrets.js
CHANGED
|
@@ -19,6 +19,10 @@ import { checkHealth } from '../core/health.js';
|
|
|
19
19
|
import { listSnapshots, restoreSnapshot, snapshotApp } from '../core/secrets-snapshots.js';
|
|
20
20
|
import { auditLog } from '../core/secrets-audit.js';
|
|
21
21
|
import { summariseSecrets, formatSecretsMotd, generateSecretsMotdScript } from '../core/secrets-motd.js';
|
|
22
|
+
import { migrateAppToV2, revertAppFromV2 } from '../core/secrets-v2-migrate.js';
|
|
23
|
+
import { cleanupV2Backups } from '../core/secrets-v2-cleanup.js';
|
|
24
|
+
import { getV2Status } from '../core/secrets-v2-ops.js';
|
|
25
|
+
import { installV2 } from '../core/secrets-v2-install.js';
|
|
22
26
|
function getDbSecretsDir() {
|
|
23
27
|
const reg = load();
|
|
24
28
|
return join(reg.infrastructure.databases.composePath, 'secrets');
|
|
@@ -46,8 +50,13 @@ export async function secretsCommand(args) {
|
|
|
46
50
|
case 'snapshots': return secretsSnapshots(rest);
|
|
47
51
|
case 'motd-init': return secretsMotdInit();
|
|
48
52
|
case 'seal-runtime': return secretsSeal(rest);
|
|
53
|
+
case 'migrate-v2': return secretsMigrateV2(rest);
|
|
54
|
+
case 'revert-v2': return secretsRevertV2(rest);
|
|
55
|
+
case 'cleanup-v2': return secretsCleanupV2(rest);
|
|
56
|
+
case 'status-v2': return secretsStatusV2(rest);
|
|
57
|
+
case 'install-v2': return secretsInstallV2(rest);
|
|
49
58
|
default:
|
|
50
|
-
error('Usage: fleet secrets <init|list|set|get|import|export|seal|unseal|rotate|rotate-key|ages|rollback|snapshots|validate|status|drift|restore>');
|
|
59
|
+
error('Usage: fleet secrets <init|list|set|get|import|export|seal|unseal|rotate|rotate-key|ages|rollback|snapshots|validate|status|drift|restore|migrate-v2|revert-v2|cleanup-v2|status-v2|install-v2>');
|
|
51
60
|
process.exit(1);
|
|
52
61
|
}
|
|
53
62
|
}
|
|
@@ -129,7 +138,7 @@ async function secretsSet(args) {
|
|
|
129
138
|
error('Empty value — aborting');
|
|
130
139
|
process.exit(1);
|
|
131
140
|
}
|
|
132
|
-
setSecret(app, key, value, { allowWeak });
|
|
141
|
+
await setSecret(app, key, value, { allowWeak });
|
|
133
142
|
success(`Set ${key} for ${app}`);
|
|
134
143
|
}
|
|
135
144
|
function secretsGet(args) {
|
|
@@ -145,7 +154,7 @@ function secretsGet(args) {
|
|
|
145
154
|
}
|
|
146
155
|
process.stdout.write(val + '\n');
|
|
147
156
|
}
|
|
148
|
-
function secretsImport(args) {
|
|
157
|
+
async function secretsImport(args) {
|
|
149
158
|
const app = args.find(a => !a.startsWith('-'));
|
|
150
159
|
const pathArg = args[1] && !args[1].startsWith('-') ? args[1] : null;
|
|
151
160
|
if (!app) {
|
|
@@ -154,7 +163,7 @@ function secretsImport(args) {
|
|
|
154
163
|
}
|
|
155
164
|
if (app === 'docker-databases') {
|
|
156
165
|
const dir = pathArg || getDbSecretsDir();
|
|
157
|
-
const count = importDbSecrets(app, dir);
|
|
166
|
+
const count = await importDbSecrets(app, dir);
|
|
158
167
|
success(`Imported ${count} secret files from ${dir}`);
|
|
159
168
|
return;
|
|
160
169
|
}
|
|
@@ -170,7 +179,7 @@ function secretsImport(args) {
|
|
|
170
179
|
else {
|
|
171
180
|
throw new SecretsError(`App not in registry and no path given: ${app}`);
|
|
172
181
|
}
|
|
173
|
-
const count = importEnvFile(app, envPath);
|
|
182
|
+
const count = await importEnvFile(app, envPath);
|
|
174
183
|
success(`Imported ${count} keys from ${envPath}`);
|
|
175
184
|
}
|
|
176
185
|
function secretsExport(args) {
|
|
@@ -187,9 +196,9 @@ function secretsUnseal() {
|
|
|
187
196
|
const count = Object.keys(manifest.apps).length;
|
|
188
197
|
success(`Unsealed ${count} apps to /run/fleet-secrets/`);
|
|
189
198
|
}
|
|
190
|
-
function secretsSeal(args) {
|
|
199
|
+
async function secretsSeal(args) {
|
|
191
200
|
const app = args.find(a => !a.startsWith('-')) || undefined;
|
|
192
|
-
const sealed = sealFromRuntime(app);
|
|
201
|
+
const sealed = await sealFromRuntime(app);
|
|
193
202
|
for (const a of sealed) {
|
|
194
203
|
success(`Sealed ${a}`);
|
|
195
204
|
}
|
|
@@ -200,7 +209,7 @@ async function secretsRotateKey(args) {
|
|
|
200
209
|
info('Cancelled');
|
|
201
210
|
return;
|
|
202
211
|
}
|
|
203
|
-
const result = rotateKey();
|
|
212
|
+
const result = await rotateKey();
|
|
204
213
|
success(`Key rotated`);
|
|
205
214
|
info(`Old: ${result.oldPubkey}`);
|
|
206
215
|
info(`New: ${result.newPubkey}`);
|
|
@@ -294,7 +303,7 @@ async function rotateOneInteractive(app, secret, opts) {
|
|
|
294
303
|
info('Cancelled');
|
|
295
304
|
return { acted: false, succeeded: false };
|
|
296
305
|
}
|
|
297
|
-
const result = performRotation(app, secret.name, newValue, {
|
|
306
|
+
const result = await performRotation(app, secret.name, newValue, {
|
|
298
307
|
dryRun: opts.dryRun,
|
|
299
308
|
dataMigrated: opts.dataMigrated,
|
|
300
309
|
});
|
|
@@ -699,3 +708,142 @@ function secretsRestore(args) {
|
|
|
699
708
|
success(`Restored vault backup for ${app}`);
|
|
700
709
|
info('Run "fleet secrets unseal" to apply to runtime');
|
|
701
710
|
}
|
|
711
|
+
async function secretsMigrateV2(args) {
|
|
712
|
+
const app = args[0];
|
|
713
|
+
if (!app || app.startsWith('-')) {
|
|
714
|
+
error('Usage: fleet secrets migrate-v2 <app> [--no-restart-app] [--dry-run]');
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
const noRestartApp = args.includes('--no-restart-app');
|
|
718
|
+
const dryRun = args.includes('--dry-run');
|
|
719
|
+
heading(`Migrating ${app} to v2 (mode=socket)`);
|
|
720
|
+
if (dryRun)
|
|
721
|
+
info('DRY RUN — no actual changes will be applied');
|
|
722
|
+
try {
|
|
723
|
+
const result = await migrateAppToV2({ app, noRestartApp, dryRun });
|
|
724
|
+
if (result.rolledBack) {
|
|
725
|
+
error(`Migration failed; rolled back from snapshot ${result.snapshotDir}`);
|
|
726
|
+
for (const step of result.steps) {
|
|
727
|
+
if (!step.ok)
|
|
728
|
+
info(` step ${step.step} (${step.name}): ${step.detail}`);
|
|
729
|
+
}
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
success(`Migrated ${app} to v2`);
|
|
733
|
+
if (result.snapshotDir)
|
|
734
|
+
info(`Snapshot: ${result.snapshotDir}`);
|
|
735
|
+
for (const step of result.steps) {
|
|
736
|
+
info(` step ${step.step} (${step.name}): ${step.ok ? 'OK' : 'FAILED'}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
error(`Migration failed: ${err.message}`);
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async function secretsRevertV2(args) {
|
|
745
|
+
const app = args[0];
|
|
746
|
+
if (!app || app.startsWith('-')) {
|
|
747
|
+
error('Usage: fleet secrets revert-v2 <app> [--snapshot <timestamp>]');
|
|
748
|
+
process.exit(1);
|
|
749
|
+
}
|
|
750
|
+
const snapIdx = args.indexOf('--snapshot');
|
|
751
|
+
const snapshotTimestamp = snapIdx >= 0 ? args[snapIdx + 1] : undefined;
|
|
752
|
+
heading(`Reverting ${app} from v2`);
|
|
753
|
+
try {
|
|
754
|
+
const result = await revertAppFromV2({ app, snapshotTimestamp });
|
|
755
|
+
if (result.ok) {
|
|
756
|
+
success(`Reverted ${app} to v1; restored from ${result.snapshotUsed}`);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
error(`Revert reported issues — see steps below`);
|
|
760
|
+
}
|
|
761
|
+
for (const step of result.steps) {
|
|
762
|
+
const status = step.ok ? 'OK' : 'FAILED';
|
|
763
|
+
info(` step ${step.step} (${step.name}): ${status}${step.detail ? ' — ' + step.detail : ''}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
error(`Revert failed: ${err.message}`);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async function secretsCleanupV2(args) {
|
|
772
|
+
const app = args[0];
|
|
773
|
+
if (!app || app.startsWith('-')) {
|
|
774
|
+
error('Usage: fleet secrets cleanup-v2 <app> [--retention-days N] [--dry-run]');
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
const retIdx = args.indexOf('--retention-days');
|
|
778
|
+
const retentionDays = retIdx >= 0 ? parseInt(args[retIdx + 1], 10) : 30;
|
|
779
|
+
const dryRun = args.includes('--dry-run');
|
|
780
|
+
if (Number.isNaN(retentionDays) || retentionDays < 0) {
|
|
781
|
+
error(`--retention-days must be a non-negative integer`);
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
heading(`Cleaning up v2 backups for ${app} (retention=${retentionDays}d)`);
|
|
785
|
+
if (dryRun)
|
|
786
|
+
info('DRY RUN — no actual deletions');
|
|
787
|
+
try {
|
|
788
|
+
const result = await cleanupV2Backups({ app, retentionDays, dryRun });
|
|
789
|
+
if (result.removedBak)
|
|
790
|
+
info(`v1 backup blob ${dryRun ? 'would be removed' : 'removed'}`);
|
|
791
|
+
info(`Snapshots removed: ${result.removedSnapshots.length}`);
|
|
792
|
+
info(`Snapshots kept: ${result.keptSnapshots.length}`);
|
|
793
|
+
for (const ts of result.removedSnapshots)
|
|
794
|
+
info(` - ${ts}`);
|
|
795
|
+
success('Cleanup complete');
|
|
796
|
+
}
|
|
797
|
+
catch (err) {
|
|
798
|
+
error(`Cleanup failed: ${err.message}`);
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function secretsInstallV2(args) {
|
|
803
|
+
const dryRun = args.includes('--dry-run');
|
|
804
|
+
heading(`Installing fleet-secrets-agent v2 host components`);
|
|
805
|
+
if (dryRun)
|
|
806
|
+
info('DRY RUN');
|
|
807
|
+
try {
|
|
808
|
+
const result = await installV2({ dryRun });
|
|
809
|
+
if (result.agentBinaryInstalled)
|
|
810
|
+
success('Agent binary installed at /usr/local/bin/fleet-agent');
|
|
811
|
+
else
|
|
812
|
+
info('Agent binary already current');
|
|
813
|
+
if (result.unitFileInstalled)
|
|
814
|
+
success('Templated unit installed at /etc/systemd/system/fleet-secrets-agent@.service');
|
|
815
|
+
else
|
|
816
|
+
info('Unit file already current');
|
|
817
|
+
if (result.daemonReloaded)
|
|
818
|
+
success('systemctl daemon-reload completed');
|
|
819
|
+
if (!result.templateParseable && !dryRun)
|
|
820
|
+
warn('Templated unit did not parse cleanly — investigate');
|
|
821
|
+
else if (!dryRun)
|
|
822
|
+
success('Templated unit verified');
|
|
823
|
+
}
|
|
824
|
+
catch (err) {
|
|
825
|
+
error(`Install failed: ${err.message}`);
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function secretsStatusV2(_args) {
|
|
830
|
+
const report = getV2Status();
|
|
831
|
+
heading('Fleet secrets v2 status');
|
|
832
|
+
info(`v1 (unseal): ${report.v1Count} apps`);
|
|
833
|
+
info(`v2 (socket): ${report.v2Count} apps`);
|
|
834
|
+
info('');
|
|
835
|
+
if (report.apps.length === 0) {
|
|
836
|
+
info('No apps in manifest');
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const headers = ['App', 'Mode', 'Agent', 'Socket', 'Keys', 'Last sealed'];
|
|
840
|
+
const rows = report.apps.map(a => [
|
|
841
|
+
a.name,
|
|
842
|
+
a.mode,
|
|
843
|
+
a.mode === 'socket' ? (a.agentActive ? 'active' : 'inactive') : '—',
|
|
844
|
+
a.mode === 'socket' ? (a.socketOk ? 'ok' : 'BAD') : '—',
|
|
845
|
+
String(a.keyCount),
|
|
846
|
+
a.lastSealedAt.slice(0, 10),
|
|
847
|
+
]);
|
|
848
|
+
table(headers, rows);
|
|
849
|
+
}
|
package/dist/commands/start.d.ts
CHANGED