@wpelevator/tug 0.5.0 → 0.7.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 +3 -3
- package/dist/agent-jobs.d.ts +41 -0
- package/dist/agent-jobs.js +161 -0
- package/dist/agent.d.ts +8 -1
- package/dist/agent.js +76 -7
- package/dist/backup-check.d.ts +11 -0
- package/dist/backup-check.js +10 -0
- package/dist/backup-config.d.ts +52 -0
- package/dist/backup-config.js +276 -0
- package/dist/backup-destination.d.ts +22 -0
- package/dist/backup-destination.js +72 -0
- package/dist/backup-job.d.ts +32 -0
- package/dist/backup-job.js +352 -0
- package/dist/backup-restore.d.ts +67 -0
- package/dist/backup-restore.js +317 -0
- package/dist/backup-scheduler.d.ts +60 -0
- package/dist/backup-scheduler.js +418 -0
- package/dist/cli-build.d.ts +1 -1
- package/dist/cli-build.js +15 -2
- package/dist/cli-config.d.ts +45 -5
- package/dist/cli-config.js +269 -19
- package/dist/cli-live-transport.d.ts +1 -1
- package/dist/cli-live-transport.js +2 -2
- package/dist/cli-live.d.ts +2 -0
- package/dist/cli-live.js +1 -0
- package/dist/cli.js +226 -16
- package/dist/compose.js +5 -2
- package/dist/container-spec.d.ts +3 -2
- package/dist/container-spec.js +9 -34
- package/dist/control.js +253 -19
- package/dist/cron.d.ts +17 -0
- package/dist/cron.js +124 -0
- package/dist/db.d.ts +70 -0
- package/dist/db.js +7 -0
- package/dist/docker-api.d.ts +7 -0
- package/dist/live.d.ts +8 -0
- package/dist/live.js +84 -0
- package/dist/normalize.d.ts +6 -0
- package/dist/normalize.js +15 -1
- package/dist/protocol.d.ts +118 -7
- package/dist/protocol.js +7 -0
- package/dist/tokens.js +10 -0
- package/package.json +2 -1
package/dist/live.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { LIVE_MAX_FRAME_BYTES } from "./protocol.js";
|
|
3
|
+
import { LABEL_JOB } from "./container-spec.js";
|
|
3
4
|
const identifier = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,255}$/;
|
|
4
5
|
function object(value) {
|
|
5
6
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -22,6 +23,8 @@ function validate(value) {
|
|
|
22
23
|
!value.command.every((arg) => typeof arg === 'string' && !arg.includes('\0') && Buffer.byteLength(arg) <= 32768) ||
|
|
23
24
|
value.command[0].length === 0)
|
|
24
25
|
throw new Error('Invalid exec command');
|
|
26
|
+
if (value.oneOff !== undefined && typeof value.oneOff !== 'boolean')
|
|
27
|
+
throw new Error('Invalid one-off flag');
|
|
25
28
|
if (value.interactive !== undefined && typeof value.interactive !== 'boolean')
|
|
26
29
|
throw new Error('Invalid interactive flag');
|
|
27
30
|
if (value.tty !== undefined && typeof value.tty !== 'boolean')
|
|
@@ -187,6 +190,8 @@ export class LiveOperationEngine {
|
|
|
187
190
|
tail: request.tail ?? 100,
|
|
188
191
|
timestamps: request.timestamps,
|
|
189
192
|
});
|
|
193
|
+
if (request.operation === 'exec' && request.oneOff)
|
|
194
|
+
return startOneOff(docker, request.container, request.command);
|
|
190
195
|
const proc = await docker.execStart(request.container, {
|
|
191
196
|
cmd: request.command,
|
|
192
197
|
stdin: request.interactive !== false,
|
|
@@ -274,3 +279,82 @@ export class LiveOperationEngine {
|
|
|
274
279
|
});
|
|
275
280
|
}
|
|
276
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Runs `command` in a new container cloned from `template`: the same image, environment (secrets included),
|
|
284
|
+
* user, working directory, network and mounts, with no published ports and none of the service's labels, so
|
|
285
|
+
* reconciliation never mistakes it for the service. The container is removed when the command ends.
|
|
286
|
+
*/
|
|
287
|
+
export async function startOneOff(docker, template, command) {
|
|
288
|
+
const source = await docker.inspectContainer(template);
|
|
289
|
+
const image = source.Config?.Image;
|
|
290
|
+
if (!image)
|
|
291
|
+
throw new Error('the service container has no image to run from');
|
|
292
|
+
const host = source.HostConfig ?? {};
|
|
293
|
+
const name = `tug-run-${Math.random().toString(36).slice(2, 10)}`;
|
|
294
|
+
const id = await docker.createContainer(name, {
|
|
295
|
+
Image: image,
|
|
296
|
+
Env: source.Config?.Env,
|
|
297
|
+
User: source.Config?.User || undefined,
|
|
298
|
+
WorkingDir: source.Config?.WorkingDir || undefined,
|
|
299
|
+
// Bypass the image's entrypoint script, like `docker exec`, so the command is exactly what was typed.
|
|
300
|
+
Entrypoint: command,
|
|
301
|
+
Cmd: [],
|
|
302
|
+
Labels: { [LABEL_JOB]: 'run' },
|
|
303
|
+
HostConfig: {
|
|
304
|
+
NetworkMode: host.NetworkMode,
|
|
305
|
+
...(host.Binds?.length ? { Binds: host.Binds } : {}),
|
|
306
|
+
...(host.Mounts?.length ? { Mounts: host.Mounts } : {}),
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
const remove = () => docker.removeContainer(id).catch(() => { });
|
|
310
|
+
try {
|
|
311
|
+
await docker.startContainer(id);
|
|
312
|
+
const logs = await docker.logs(id, { follow: true });
|
|
313
|
+
let killed = false;
|
|
314
|
+
const proc = Object.assign(new EventEmitter(), {
|
|
315
|
+
stdin: null,
|
|
316
|
+
stdout: logs.stdout,
|
|
317
|
+
stderr: logs.stderr,
|
|
318
|
+
kill: () => {
|
|
319
|
+
killed = true;
|
|
320
|
+
logs.kill();
|
|
321
|
+
void remove();
|
|
322
|
+
return true;
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
const drained = (stream) => new Promise((resolve) => {
|
|
326
|
+
if (stream.readableEnded || stream.destroyed)
|
|
327
|
+
return resolve();
|
|
328
|
+
stream.once('end', resolve);
|
|
329
|
+
stream.once('close', resolve);
|
|
330
|
+
});
|
|
331
|
+
// The exit code comes from the container, not from the log stream, which only ends once it is consumed.
|
|
332
|
+
void (async () => {
|
|
333
|
+
let code = -1;
|
|
334
|
+
while (!killed) {
|
|
335
|
+
const state = (await docker.inspectContainer(id).catch(() => undefined))?.State;
|
|
336
|
+
if (!state)
|
|
337
|
+
break;
|
|
338
|
+
if (!state.Running && state.Status !== 'created') {
|
|
339
|
+
code = state.ExitCode ?? -1;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
343
|
+
}
|
|
344
|
+
if (killed)
|
|
345
|
+
return;
|
|
346
|
+
// Give the consumer a moment to read the last output before the container goes away.
|
|
347
|
+
await Promise.race([
|
|
348
|
+
Promise.all([drained(logs.stdout), drained(logs.stderr)]),
|
|
349
|
+
new Promise((resolve) => setTimeout(resolve, 1500)),
|
|
350
|
+
]);
|
|
351
|
+
await remove();
|
|
352
|
+
proc.emit('close', code, null);
|
|
353
|
+
})();
|
|
354
|
+
return proc;
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
await remove();
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
package/dist/normalize.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export declare function sentinelNames(value: unknown): string[];
|
|
|
10
10
|
export declare function substituteSentinels(value: string, secrets: Record<string, string>): string;
|
|
11
11
|
/** Arguments of the `docker compose` call that renders `file` canonically (as YAML, which keeps `x-tug`). */
|
|
12
12
|
export declare function composeConfigArgs(file: string, project?: string): string[];
|
|
13
|
+
/**
|
|
14
|
+
* `docker compose config` writes every literal `$` as `$$` so its output can be fed back to Compose. Tug does not
|
|
15
|
+
* interpolate again, so a command like `sh -c 'echo $$HOME'` would reach the container with the doubled dollar,
|
|
16
|
+
* where a shell reads `$$` as its own process ID. Undoing the escaping is exact: output never contains a bare `$`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function unescapeDollars<T>(value: T): T;
|
|
13
19
|
/**
|
|
14
20
|
* Renders `file` with Compose and returns the canonical definition. Every name in `secretNames` is set to
|
|
15
21
|
* its sentinel in the environment, so it wins over a same-named local variable.
|
package/dist/normalize.js
CHANGED
|
@@ -41,6 +41,20 @@ export function composeConfigArgs(file, project) {
|
|
|
41
41
|
'--no-path-resolution',
|
|
42
42
|
];
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* `docker compose config` writes every literal `$` as `$$` so its output can be fed back to Compose. Tug does not
|
|
46
|
+
* interpolate again, so a command like `sh -c 'echo $$HOME'` would reach the container with the doubled dollar,
|
|
47
|
+
* where a shell reads `$$` as its own process ID. Undoing the escaping is exact: output never contains a bare `$`.
|
|
48
|
+
*/
|
|
49
|
+
export function unescapeDollars(value) {
|
|
50
|
+
if (typeof value === 'string')
|
|
51
|
+
return value.replaceAll('$$', '$');
|
|
52
|
+
if (Array.isArray(value))
|
|
53
|
+
return value.map(unescapeDollars);
|
|
54
|
+
if (value && typeof value === 'object')
|
|
55
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, unescapeDollars(item)]));
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
44
58
|
/**
|
|
45
59
|
* Renders `file` with Compose and returns the canonical definition. Every name in `secretNames` is set to
|
|
46
60
|
* its sentinel in the environment, so it wins over a same-named local variable.
|
|
@@ -71,5 +85,5 @@ export async function normalizeCompose(file, secretNames, options = {}) {
|
|
|
71
85
|
}
|
|
72
86
|
if (!parsed || typeof parsed !== 'object' || !parsed.services)
|
|
73
87
|
throw new Error(`${file} defines no services`);
|
|
74
|
-
return parsed;
|
|
88
|
+
return unescapeDollars(parsed);
|
|
75
89
|
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -24,6 +24,42 @@ export interface IngressSpec {
|
|
|
24
24
|
cert?: string;
|
|
25
25
|
key?: string;
|
|
26
26
|
}
|
|
27
|
+
/** What a backup captures: the output of a command run in a service, or the files of a volume. */
|
|
28
|
+
export type BackupType = 'command' | 'volume';
|
|
29
|
+
/** A restic keep policy; at least one key is required. */
|
|
30
|
+
export interface BackupRetention {
|
|
31
|
+
last?: number;
|
|
32
|
+
hourly?: number;
|
|
33
|
+
daily?: number;
|
|
34
|
+
weekly?: number;
|
|
35
|
+
monthly?: number;
|
|
36
|
+
yearly?: number;
|
|
37
|
+
}
|
|
38
|
+
/** `x-tug.backup` on a service or top-level volume, as an object. */
|
|
39
|
+
export interface BackupSpec {
|
|
40
|
+
/** Five-field cron in UTC, or `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly`. */
|
|
41
|
+
schedule?: string;
|
|
42
|
+
retention?: BackupRetention;
|
|
43
|
+
/** Seconds of deterministic per-target delay after the scheduled minute. */
|
|
44
|
+
jitter?: number;
|
|
45
|
+
/** Hard limit for one run, such as `30m` or `2h`. */
|
|
46
|
+
timeout?: string | number;
|
|
47
|
+
/** Memory limit of the restic helper container, such as `256m` (default `512m`). */
|
|
48
|
+
memory?: string;
|
|
49
|
+
/** Services only: run in the service's container; its standard output is the backup. A string runs through `sh -c`. */
|
|
50
|
+
command?: string | string[];
|
|
51
|
+
/** Services only: run in the service container to restore; the stored file arrives on its standard input. */
|
|
52
|
+
restore?: string | string[];
|
|
53
|
+
/** Services only: name of the file inside the snapshot (default `<service>.out`). */
|
|
54
|
+
filename?: string;
|
|
55
|
+
/** Services only: text that must appear in the last 4 KiB of output, so a command cut short is not kept as a good backup. */
|
|
56
|
+
verify?: string;
|
|
57
|
+
/** Volumes only: restic exclude patterns. */
|
|
58
|
+
exclude?: string[];
|
|
59
|
+
/** Services to hold during a restore. */
|
|
60
|
+
writers?: string[];
|
|
61
|
+
}
|
|
62
|
+
export type BackupConfig = boolean | string | BackupSpec;
|
|
27
63
|
export type RolloutStrategy = 'recreate' | 'rolling';
|
|
28
64
|
/** `x-tug.rollout`: how a service is replaced on a new deployment. `recreate` (default) is `docker compose up -d`. */
|
|
29
65
|
export interface RolloutSpec {
|
|
@@ -39,11 +75,14 @@ export interface ServiceExtension {
|
|
|
39
75
|
health?: HealthCheck;
|
|
40
76
|
ingress?: IngressSpec;
|
|
41
77
|
rollout?: RolloutSpec;
|
|
78
|
+
backup?: BackupConfig;
|
|
42
79
|
}
|
|
43
80
|
/** The top-level `x-tug` extension of a Compose file. */
|
|
44
81
|
export interface ProjectExtension {
|
|
45
82
|
/** `auto` creates a rollback deployment when a deployment fails on health; `manual` (default) only reports it. */
|
|
46
83
|
rollback?: 'auto' | 'manual';
|
|
84
|
+
/** Defaults for every `x-tug.backup` target; they enable nothing by themselves. */
|
|
85
|
+
backup?: Pick<BackupSpec, 'schedule' | 'retention'>;
|
|
47
86
|
}
|
|
48
87
|
/** The `x-tug` extension on a top-level Compose volume. */
|
|
49
88
|
export interface VolumeExtension {
|
|
@@ -51,10 +90,7 @@ export interface VolumeExtension {
|
|
|
51
90
|
driver?: string;
|
|
52
91
|
persistent?: boolean;
|
|
53
92
|
placement?: Placement;
|
|
54
|
-
|
|
55
|
-
address?: string;
|
|
56
|
-
/** Directory exported by the NFS server on the exporter node (default `/srv/tug/<volume>`). */
|
|
57
|
-
export?: string;
|
|
93
|
+
backup?: BackupConfig;
|
|
58
94
|
}
|
|
59
95
|
export type BuildConfig = string | {
|
|
60
96
|
context?: string;
|
|
@@ -130,7 +166,74 @@ export declare const ACTIVE_DEPLOYMENT_STATES: readonly ["uploading", "building"
|
|
|
130
166
|
/** Deployment states a CLI wait loop stops on. */
|
|
131
167
|
export declare const TERMINAL_DEPLOYMENT_STATES: readonly ["succeeded", "failed", "build_failed", "cancelled"];
|
|
132
168
|
export type DeploymentState = (typeof ACTIVE_DEPLOYMENT_STATES)[number] | (typeof TERMINAL_DEPLOYMENT_STATES)[number];
|
|
133
|
-
export type CommandType = 'diagnostics.run' | 'service.restart' | 'logs.fetch' | 'volume.delete' | 'wg.rotate-key';
|
|
169
|
+
export type CommandType = 'diagnostics.run' | 'service.restart' | 'logs.fetch' | 'volume.delete' | 'wg.rotate-key' | 'backup.check' | 'backup.run' | 'backup.restore' | 'backup.list';
|
|
170
|
+
/** Commands whose helper containers need the backup destination's credentials. */
|
|
171
|
+
export declare const BACKUP_COMMAND_TYPES: readonly CommandType[];
|
|
172
|
+
/** `GET /agent/v1/backup/credentials`: environment for the restic helper of one running backup command. */
|
|
173
|
+
export interface BackupCredentialsResponse {
|
|
174
|
+
env: Record<string, string>;
|
|
175
|
+
}
|
|
176
|
+
/** Why a command may be refused or a run marked missed; recorded on the run. */
|
|
177
|
+
export type BackupMissReason = 'node_offline' | 'control_down' | 'deadline';
|
|
178
|
+
export type JobPhase = 'queued' | 'pulling' | 'checking' | 'dumping' | 'uploading' | 'forgetting' | 'restoring';
|
|
179
|
+
/** Payload of `backup.check`, `backup.run` and `backup.restore`. Restic credentials are never in a payload. */
|
|
180
|
+
export interface BackupJobPayload {
|
|
181
|
+
runId: string;
|
|
182
|
+
project: string;
|
|
183
|
+
/** `service:db` or `volume:uploads`; empty for `backup.check`. */
|
|
184
|
+
target: string;
|
|
185
|
+
type: BackupType;
|
|
186
|
+
/** The project's restic repository, `<destination>/<project>`. */
|
|
187
|
+
repository: string;
|
|
188
|
+
resticImage: string;
|
|
189
|
+
/** Used as restic's `--host`, so each node keeps its own snapshot chain. */
|
|
190
|
+
node: string;
|
|
191
|
+
tags: string[];
|
|
192
|
+
retention?: BackupRetention;
|
|
193
|
+
/** Command targets: what to run in the service's container, and how to name and check its output. */
|
|
194
|
+
command?: {
|
|
195
|
+
service: string;
|
|
196
|
+
argv: string[];
|
|
197
|
+
filename: string;
|
|
198
|
+
verify?: string;
|
|
199
|
+
/** Run in the service container with the snapshot's file on stdin when restoring. */
|
|
200
|
+
restoreArgv?: string[];
|
|
201
|
+
};
|
|
202
|
+
/** Volume targets: the Docker volume to mount read-only; it must already exist on the node. */
|
|
203
|
+
volume?: {
|
|
204
|
+
name: string;
|
|
205
|
+
dockerVolume: string;
|
|
206
|
+
exclude?: string[];
|
|
207
|
+
};
|
|
208
|
+
limits: {
|
|
209
|
+
memory: string;
|
|
210
|
+
timeoutSec: number;
|
|
211
|
+
};
|
|
212
|
+
restore?: {
|
|
213
|
+
snapshotId: string;
|
|
214
|
+
mode: 'in-place' | 'scratch';
|
|
215
|
+
scratchVolume?: string;
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export interface BackupJobResult {
|
|
219
|
+
runId: string;
|
|
220
|
+
snapshotId?: string;
|
|
221
|
+
addedBytes?: number;
|
|
222
|
+
totalBytes?: number;
|
|
223
|
+
durationMs: number;
|
|
224
|
+
resticVersion?: string;
|
|
225
|
+
/** Repository state for `backup.check`. */
|
|
226
|
+
repository?: 'ok' | 'initialized';
|
|
227
|
+
warnings?: string[];
|
|
228
|
+
}
|
|
229
|
+
/** Live jobs in `StatusReport.jobs`, so control can tell "running" from "lost". */
|
|
230
|
+
export interface JobStatus {
|
|
231
|
+
commandId: string;
|
|
232
|
+
runId: string;
|
|
233
|
+
phase: JobPhase;
|
|
234
|
+
startedAt: string;
|
|
235
|
+
bytes?: number;
|
|
236
|
+
}
|
|
134
237
|
/** A `commands` table row as delivered in `GET /agent/v1/work`. */
|
|
135
238
|
export interface Command {
|
|
136
239
|
id: string;
|
|
@@ -173,7 +276,7 @@ export type CommandResultPayload = {
|
|
|
173
276
|
deleted: true;
|
|
174
277
|
} | {
|
|
175
278
|
publicKey: string;
|
|
176
|
-
} | {
|
|
279
|
+
} | BackupJobResult | {
|
|
177
280
|
error: string;
|
|
178
281
|
};
|
|
179
282
|
export interface CommandResult {
|
|
@@ -206,6 +309,8 @@ export type LiveRequest = {
|
|
|
206
309
|
tty: boolean;
|
|
207
310
|
columns?: number;
|
|
208
311
|
rows?: number;
|
|
312
|
+
/** Run the command in a new container cloned from the service's, instead of in the running one. */
|
|
313
|
+
oneOff?: boolean;
|
|
209
314
|
} | {
|
|
210
315
|
operation: 'logs';
|
|
211
316
|
project: string;
|
|
@@ -327,6 +432,8 @@ export interface WorkResponse {
|
|
|
327
432
|
ingress?: IngressWork;
|
|
328
433
|
/** Host addresses for published ports that name no host IP. */
|
|
329
434
|
ports?: PortBinding;
|
|
435
|
+
/** Per project, services that must stay stopped because a restore is rewriting their data. */
|
|
436
|
+
holds?: Record<string, string[]>;
|
|
330
437
|
}
|
|
331
438
|
/**
|
|
332
439
|
* Where a node's published ports listen when a `ports` entry names no host IP. Entries with an
|
|
@@ -393,7 +500,9 @@ export interface IngressStatus {
|
|
|
393
500
|
error?: string;
|
|
394
501
|
}
|
|
395
502
|
export type ProjectHealth = 'serving' | 'skipped' | 'failed';
|
|
396
|
-
export type RolloutState = 'pending' | 'pulling' | 'starting' | 'health_checking' | 'serving'
|
|
503
|
+
export type RolloutState = 'pending' | 'pulling' | 'starting' | 'health_checking' | 'serving'
|
|
504
|
+
/** Stopped on purpose while a restore rewrites its data. */
|
|
505
|
+
| 'held';
|
|
397
506
|
export interface ServiceState {
|
|
398
507
|
rolloutState: RolloutState;
|
|
399
508
|
image?: string;
|
|
@@ -419,6 +528,8 @@ export interface StatusReport {
|
|
|
419
528
|
projects: ProjectStatus[];
|
|
420
529
|
commandResults: CommandResult[];
|
|
421
530
|
ingress?: IngressStatus;
|
|
531
|
+
/** Backup jobs still running on this node; absent when none. */
|
|
532
|
+
jobs?: JobStatus[];
|
|
422
533
|
}
|
|
423
534
|
export interface SecretsResponse {
|
|
424
535
|
registryAuth?: Record<string, {
|
package/dist/protocol.js
CHANGED
|
@@ -16,6 +16,13 @@ export const TERMINAL_DEPLOYMENT_STATES = [
|
|
|
16
16
|
'build_failed',
|
|
17
17
|
'cancelled',
|
|
18
18
|
];
|
|
19
|
+
/** Commands whose helper containers need the backup destination's credentials. */
|
|
20
|
+
export const BACKUP_COMMAND_TYPES = [
|
|
21
|
+
'backup.check',
|
|
22
|
+
'backup.run',
|
|
23
|
+
'backup.restore',
|
|
24
|
+
'backup.list',
|
|
25
|
+
];
|
|
19
26
|
export const LIVE_MAX_FRAME_BYTES = 256 * 1024;
|
|
20
27
|
export const LIVE_MAX_BUFFERED_BYTES = 1024 * 1024;
|
|
21
28
|
export const LIVE_MAX_SESSIONS = 64;
|
package/dist/tokens.js
CHANGED
|
@@ -42,6 +42,16 @@ export const ROUTE_ROLES = {
|
|
|
42
42
|
'POST /api/v1/nodes/:name/rotate-wg-key': 'admin',
|
|
43
43
|
'POST /api/v1/nodes/:name/ingress': 'admin',
|
|
44
44
|
'DELETE /api/v1/nodes/:name': 'admin',
|
|
45
|
+
'POST /api/v1/backups/run': 'deploy',
|
|
46
|
+
'GET /api/v1/backups': 'read',
|
|
47
|
+
'POST /api/v1/restores': 'admin',
|
|
48
|
+
'GET /api/v1/restores': 'read',
|
|
49
|
+
'POST /api/v1/restores/:id/release': 'admin',
|
|
50
|
+
'POST /api/v1/restores/:id/resume': 'admin',
|
|
51
|
+
'GET /api/v1/backup/destination': 'admin',
|
|
52
|
+
'POST /api/v1/backup/destination': 'admin',
|
|
53
|
+
'DELETE /api/v1/backup/destination': 'admin',
|
|
54
|
+
'GET /api/v1/backup/key': 'admin',
|
|
45
55
|
'POST /api/v1/control/backups/run': 'admin',
|
|
46
56
|
'POST /api/v1/control/restore': 'admin',
|
|
47
57
|
'GET /api/v1/diagnostics': 'admin',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wpelevator/tug",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Portable VPS deployments with Docker Compose",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"test:integration": "bash tools/scripts/integration-test.sh",
|
|
30
30
|
"test:integration:network": "bash tools/scripts/integration-network-test.sh",
|
|
31
31
|
"test:integration:agent": "node --test 'tests/integration/**/*.test.ts'",
|
|
32
|
+
"test:integration:backups": "node --test tests/integration/backup-job.test.ts tests/integration/run-job.test.ts",
|
|
32
33
|
"test:smoke:dist": "npm run build && CLI_ENTRY=dist/cli.js node --test src/cli.templates.test.ts",
|
|
33
34
|
"changeset": "changeset",
|
|
34
35
|
"version-packages": "changeset version && npm install --package-lock-only --ignore-scripts",
|