@ninedeploy/sdk 0.18.25 → 0.18.27
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/dist/databaseImport.d.ts +219 -6
- package/dist/databaseImport.d.ts.map +1 -1
- package/dist/databaseImport.js +43 -13
- package/dist/databaseImport.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/auth.d.ts +3 -2
- package/dist/resources/auth.d.ts.map +1 -1
- package/dist/resources/auth.js.map +1 -1
- package/dist/resources/databases.d.ts +27 -1
- package/dist/resources/databases.d.ts.map +1 -1
- package/dist/resources/databases.js +21 -0
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/services.d.ts +3 -1
- package/dist/resources/services.d.ts.map +1 -1
- package/dist/resources/services.js +1 -0
- package/dist/resources/services.js.map +1 -1
- package/dist/resources/settings.d.ts +9 -1
- package/dist/resources/settings.d.ts.map +1 -1
- package/dist/resources/settings.js +4 -0
- package/dist/resources/settings.js.map +1 -1
- package/dist/resources/system.d.ts +11 -1
- package/dist/resources/system.d.ts.map +1 -1
- package/dist/resources/system.js +4 -0
- package/dist/resources/system.js.map +1 -1
- package/dist/resources/volumes.d.ts +19 -1
- package/dist/resources/volumes.d.ts.map +1 -1
- package/dist/resources/volumes.js +10 -0
- package/dist/resources/volumes.js.map +1 -1
- package/package.json +2 -2
- package/src/databaseImport.ts +87 -19
- package/src/index.ts +2 -0
- package/src/resources/auth.ts +4 -3
- package/src/resources/databases.ts +39 -0
- package/src/resources/services.ts +4 -0
- package/src/resources/settings.ts +14 -0
- package/src/resources/system.ts +10 -0
- package/src/resources/volumes.ts +28 -0
package/src/databaseImport.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
DatabaseImport,
|
|
6
6
|
DatabaseImportCreate,
|
|
7
7
|
DatabaseImportOptions,
|
|
8
|
+
VolumeImport,
|
|
9
|
+
VolumeImportCreate,
|
|
8
10
|
} from '@ninedeploy/schemas';
|
|
9
11
|
import { NineDeployError } from './errors.js';
|
|
10
12
|
import type { Transport } from './transport.js';
|
|
@@ -18,8 +20,11 @@ export const DATABASE_IMPORT_FINISHED_STATUSES: ReadonlyArray<DatabaseImport['st
|
|
|
18
20
|
'expired',
|
|
19
21
|
];
|
|
20
22
|
|
|
21
|
-
/**
|
|
22
|
-
export
|
|
23
|
+
/** A1: a volume import that no longer changes (`VolumeImport['status']`). */
|
|
24
|
+
export const VOLUME_IMPORT_FINISHED_STATUSES: ReadonlyArray<VolumeImport['status']> = ['completed', 'failed', 'cancelled', 'expired'];
|
|
25
|
+
|
|
26
|
+
/** 0.14: `databases.imports.wait` (and A1 `volumeImports.wait`) polling options. */
|
|
27
|
+
export interface ImportWaitOptions<Row = DatabaseImport> {
|
|
23
28
|
/**
|
|
24
29
|
* `finished` (default) waits for a final status; `uploaded` returns as soon
|
|
25
30
|
* as the import leaves `uploading` (an S3 download landing in `pending`).
|
|
@@ -30,7 +35,7 @@ export interface ImportWaitOptions {
|
|
|
30
35
|
/** Give up after this long (default: never). Throws `import_wait_timeout`. */
|
|
31
36
|
timeoutMs?: number;
|
|
32
37
|
/** Called with every polled row. */
|
|
33
|
-
onStatus?: (row:
|
|
38
|
+
onStatus?: (row: Row) => void;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
/**
|
|
@@ -60,6 +65,22 @@ export interface ImportFileOptions {
|
|
|
60
65
|
onProgress?: (progress: { receivedBytes: number; sizeBytes: number; row: DatabaseImport }) => void;
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
/**
|
|
69
|
+
* A1: `volumeImports.importFile` options: a gzipped tar of a volume's
|
|
70
|
+
* contents, restored into a service's data volume. The sha256 is required
|
|
71
|
+
* (the server checks it before the volume is touched) unless resuming.
|
|
72
|
+
*/
|
|
73
|
+
export interface VolumeImportFileOptions {
|
|
74
|
+
sizeBytes?: number;
|
|
75
|
+
filename?: string;
|
|
76
|
+
sha256?: string;
|
|
77
|
+
resumeImportId?: number;
|
|
78
|
+
start?: boolean;
|
|
79
|
+
poll?: boolean | Omit<ImportWaitOptions<VolumeImport>, 'until'>;
|
|
80
|
+
onCreated?: (row: VolumeImport) => void;
|
|
81
|
+
onProgress?: (progress: { receivedBytes: number; sizeBytes: number; row: VolumeImport }) => void;
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
/** 0.14: the import bytes from `offset` on (a factory seeks; the rest skip). */
|
|
64
85
|
async function* sourceFrom(data: ImportFileData, offset: number): AsyncIterable<Uint8Array> {
|
|
65
86
|
if (data instanceof Uint8Array) {
|
|
@@ -102,29 +123,51 @@ async function* rechunk(source: AsyncIterable<Uint8Array>, size: number): AsyncI
|
|
|
102
123
|
if (len > 0) yield buf.subarray(0, len);
|
|
103
124
|
}
|
|
104
125
|
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
interface UploadRow {
|
|
127
|
+
id: number;
|
|
128
|
+
status: string;
|
|
129
|
+
sizeBytes: number;
|
|
130
|
+
receivedBytes: number;
|
|
131
|
+
chunkSize: number;
|
|
132
|
+
}
|
|
133
|
+
interface UploadFileOptions<Row> {
|
|
134
|
+
sizeBytes?: number;
|
|
135
|
+
resumeImportId?: number;
|
|
136
|
+
start?: boolean;
|
|
137
|
+
poll?: boolean | Omit<ImportWaitOptions<Row>, 'until'>;
|
|
138
|
+
onCreated?: (row: Row) => void;
|
|
139
|
+
onProgress?: (progress: { receivedBytes: number; sizeBytes: number; row: Row }) => void;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The chunked, resumable upload shared by dump imports and volume imports:
|
|
144
|
+
* create (or resume) a row under `base(id)`, send ordered chunks of the
|
|
145
|
+
* size the server advertised, start, and optionally poll to a final status.
|
|
146
|
+
*/
|
|
147
|
+
function chunkedUploads<Row extends UploadRow, Opts extends UploadFileOptions<Row>>(
|
|
148
|
+
t: Transport,
|
|
149
|
+
cfg: { base: (id: number) => string; createBody: (sizeBytes: number, opts: Opts) => unknown; finished: ReadonlyArray<string> },
|
|
150
|
+
) {
|
|
107
151
|
const { get, send, request } = t;
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const getImport = (id: number, importId: number) => get<DatabaseImport>(importPath(id, importId));
|
|
152
|
+
const importPath = (id: number, importId?: number) => `${cfg.base(id)}${importId === undefined ? '' : `/${importId}`}`;
|
|
153
|
+
const getImport = (id: number, importId: number) => get<Row>(importPath(id, importId));
|
|
111
154
|
/** An 8 MiB chunk over a slow uplink outlasts the default 30 s budget. */
|
|
112
155
|
const CHUNK_TIMEOUT_MS = 10 * 60_000;
|
|
113
156
|
const uploadChunk = (id: number, importId: number, index: number, chunk: Uint8Array) =>
|
|
114
|
-
request<
|
|
115
|
-
const startImport = (id: number, importId: number) => send<
|
|
157
|
+
request<Row>(`${importPath(id, importId)}/chunks/${index}`, { method: 'PUT', raw: chunk }, CHUNK_TIMEOUT_MS);
|
|
158
|
+
const startImport = (id: number, importId: number) => send<Row>('POST', `${importPath(id, importId)}/start`);
|
|
116
159
|
const sleep = (ms: number) =>
|
|
117
160
|
new Promise<void>((resolve) => {
|
|
118
161
|
(globalThis as unknown as { setTimeout: (fn: () => void, ms: number) => unknown }).setTimeout(resolve, ms);
|
|
119
162
|
});
|
|
120
163
|
|
|
121
|
-
async function waitForImport(id: number, importId: number, o: ImportWaitOptions = {}): Promise<
|
|
164
|
+
async function waitForImport(id: number, importId: number, o: ImportWaitOptions<Row> = {}): Promise<Row> {
|
|
122
165
|
const interval = o.intervalMs ?? 2000;
|
|
123
166
|
const deadline = o.timeoutMs === undefined ? Number.POSITIVE_INFINITY : Date.now() + o.timeoutMs;
|
|
124
167
|
for (;;) {
|
|
125
168
|
const row = await getImport(id, importId);
|
|
126
169
|
o.onStatus?.(row);
|
|
127
|
-
const done = o.until === 'uploaded' ? row.status !== 'uploading' :
|
|
170
|
+
const done = o.until === 'uploaded' ? row.status !== 'uploading' : cfg.finished.includes(row.status);
|
|
128
171
|
if (done) return row;
|
|
129
172
|
if (Date.now() + interval > deadline) {
|
|
130
173
|
throw new NineDeployError(0, 'import_wait_timeout', `Import ${importId} is still ${row.status}`);
|
|
@@ -133,18 +176,14 @@ export function createImportHelpers(t: Transport) {
|
|
|
133
176
|
}
|
|
134
177
|
}
|
|
135
178
|
|
|
136
|
-
async function importFile(id: number, data: ImportFileData, opts:
|
|
179
|
+
async function importFile(id: number, data: ImportFileData, opts: Opts = {} as Opts): Promise<Row> {
|
|
137
180
|
const declared = data instanceof Uint8Array ? data.length : opts.sizeBytes;
|
|
138
|
-
let row:
|
|
181
|
+
let row: Row;
|
|
139
182
|
if (opts.resumeImportId !== undefined) {
|
|
140
183
|
row = await getImport(id, opts.resumeImportId);
|
|
141
184
|
} else {
|
|
142
185
|
if (declared === undefined) throw new NineDeployError(0, 'import_size_required', 'sizeBytes is required for a streamed import');
|
|
143
|
-
|
|
144
|
-
if (opts.sha256 !== undefined) input.sha256 = opts.sha256;
|
|
145
|
-
if (opts.filename !== undefined) input.filename = opts.filename;
|
|
146
|
-
if (opts.options !== undefined) input.options = opts.options;
|
|
147
|
-
row = await send<DatabaseImport>('POST', importPath(id), input);
|
|
186
|
+
row = await send<Row>('POST', importPath(id), cfg.createBody(declared, opts));
|
|
148
187
|
opts.onCreated?.(row);
|
|
149
188
|
}
|
|
150
189
|
const size = row.sizeBytes;
|
|
@@ -172,3 +211,32 @@ export function createImportHelpers(t: Transport) {
|
|
|
172
211
|
|
|
173
212
|
return { importPath, getImport, uploadChunk, startImport, waitForImport, importFile };
|
|
174
213
|
}
|
|
214
|
+
|
|
215
|
+
/** Chunked dump-import helpers bound to one transport (used by `databases`). */
|
|
216
|
+
export function createImportHelpers(t: Transport) {
|
|
217
|
+
return chunkedUploads<DatabaseImport, ImportFileOptions>(t, {
|
|
218
|
+
base: (id) => `/v1/databases/${id}/imports`,
|
|
219
|
+
createBody: (sizeBytes, opts) => {
|
|
220
|
+
const input: DatabaseImportCreate = { source: 'upload', sizeBytes };
|
|
221
|
+
if (opts.sha256 !== undefined) input.sha256 = opts.sha256;
|
|
222
|
+
if (opts.filename !== undefined) input.filename = opts.filename;
|
|
223
|
+
if (opts.options !== undefined) input.options = opts.options;
|
|
224
|
+
return input;
|
|
225
|
+
},
|
|
226
|
+
finished: DATABASE_IMPORT_FINISHED_STATUSES,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A1: chunked volume-import helpers (`/v1/services/:id/volume-imports`). */
|
|
231
|
+
export function createVolumeImportHelpers(t: Transport) {
|
|
232
|
+
return chunkedUploads<VolumeImport, VolumeImportFileOptions>(t, {
|
|
233
|
+
base: (id) => `/v1/services/${id}/volume-imports`,
|
|
234
|
+
createBody: (sizeBytes, opts) => {
|
|
235
|
+
if (opts.sha256 === undefined) throw new NineDeployError(0, 'import_sha256_required', "A volume import needs the archive's sha256");
|
|
236
|
+
const input: VolumeImportCreate = { sizeBytes, sha256: opts.sha256 };
|
|
237
|
+
if (opts.filename !== undefined) input.filename = opts.filename;
|
|
238
|
+
return input;
|
|
239
|
+
},
|
|
240
|
+
finished: VOLUME_IMPORT_FINISHED_STATUSES,
|
|
241
|
+
});
|
|
242
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -76,12 +76,14 @@ export type {
|
|
|
76
76
|
|
|
77
77
|
export {
|
|
78
78
|
DATABASE_IMPORT_FINISHED_STATUSES,
|
|
79
|
+
VOLUME_IMPORT_FINISHED_STATUSES,
|
|
79
80
|
} from './databaseImport.js';
|
|
80
81
|
|
|
81
82
|
export type {
|
|
82
83
|
ImportWaitOptions,
|
|
83
84
|
ImportFileData,
|
|
84
85
|
ImportFileOptions,
|
|
86
|
+
VolumeImportFileOptions,
|
|
85
87
|
} from './databaseImport.js';
|
|
86
88
|
|
|
87
89
|
export type {
|
package/src/resources/auth.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
PublicUser,
|
|
15
15
|
Refresh,
|
|
16
16
|
Register,
|
|
17
|
+
Setup,
|
|
17
18
|
Session,
|
|
18
19
|
UserCreate,
|
|
19
20
|
WorkspaceEntry,
|
|
@@ -48,7 +49,7 @@ import type { Transport } from '../transport.js';
|
|
|
48
49
|
export interface AuthApi {
|
|
49
50
|
auth: {
|
|
50
51
|
/** `setupTokenRequired` (0.16): the first-run bootstrap needs the installer's setup token. */
|
|
51
|
-
status: () => Promise<{ initialized: boolean; allowRegistration?: boolean; setupTokenRequired?: boolean }>;
|
|
52
|
+
status: () => Promise<{ initialized: boolean; allowRegistration?: boolean; setupTokenRequired?: boolean; acmeConfigured?: boolean }>;
|
|
52
53
|
/**
|
|
53
54
|
* Introspect the current bearer token. Returns the
|
|
54
55
|
* token's id (when opaque), name, scopes, expiry and
|
|
@@ -58,7 +59,7 @@ export interface AuthApi {
|
|
|
58
59
|
* tools.
|
|
59
60
|
*/
|
|
60
61
|
introspectToken: () => Promise<TokenIntrospection>;
|
|
61
|
-
setup: (input:
|
|
62
|
+
setup: (input: Setup) => Promise<Session>;
|
|
62
63
|
register: (input: Register) => Promise<Session>;
|
|
63
64
|
login: (input: Login) => Promise<Session>;
|
|
64
65
|
refresh: (input: Refresh) => Promise<Session>;
|
|
@@ -249,7 +250,7 @@ export function authApi(t: Transport): AuthApi {
|
|
|
249
250
|
const { get, send, request } = t;
|
|
250
251
|
return {
|
|
251
252
|
auth: {
|
|
252
|
-
status: () => get<{ initialized: boolean; allowRegistration?: boolean; setupTokenRequired?: boolean }>('/v1/auth/status'),
|
|
253
|
+
status: () => get<{ initialized: boolean; allowRegistration?: boolean; setupTokenRequired?: boolean; acmeConfigured?: boolean }>('/v1/auth/status'),
|
|
253
254
|
introspectToken: () => get<TokenIntrospection>('/v1/auth/token'),
|
|
254
255
|
setup: (input) => send<Session>('POST', '/v1/setup', input),
|
|
255
256
|
register: (input) => send<Session>('POST', '/v1/auth/register', input),
|
|
@@ -15,6 +15,8 @@ import type {
|
|
|
15
15
|
PublicAccessStatus,
|
|
16
16
|
DatabaseImport,
|
|
17
17
|
DatabaseImportCreate,
|
|
18
|
+
DatabaseUpgrade,
|
|
19
|
+
DatabaseUpgradeOptions,
|
|
18
20
|
BackupDestinationObject,
|
|
19
21
|
BackupReplica,
|
|
20
22
|
BackupReplicasView,
|
|
@@ -104,6 +106,25 @@ export interface DatabasesApi {
|
|
|
104
106
|
recoveries: (id: number) => Promise<PitrRecovery[]>;
|
|
105
107
|
recovery: (id: number, recoveryId: number) => Promise<PitrRecovery>;
|
|
106
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* O2: major-version upgrades (PostgreSQL on the panel host). `options`
|
|
111
|
+
* reads the running cluster's major and what it can move to. `start`
|
|
112
|
+
* checks the cluster, then answers 202 with the running upgrade: the
|
|
113
|
+
* database is stopped, dumped (the dump is kept in its backups), restored
|
|
114
|
+
* into a new volume on the target major and compared before the switch.
|
|
115
|
+
* The previous volume stays until `finalize`; until then `rollback`
|
|
116
|
+
* points the database back at it. Starting, rolling back and finalizing
|
|
117
|
+
* need `admin` on the database.
|
|
118
|
+
*/
|
|
119
|
+
upgrades: {
|
|
120
|
+
list: (id: number) => Promise<DatabaseUpgrade[]>;
|
|
121
|
+
options: (id: number) => Promise<DatabaseUpgradeOptions>;
|
|
122
|
+
start: (id: number, input: { toMajor: number }) => Promise<DatabaseUpgrade>;
|
|
123
|
+
rollback: (id: number, upgradeId: number) => Promise<DatabaseUpgrade>;
|
|
124
|
+
finalize: (id: number, upgradeId: number) => Promise<DatabaseUpgrade & { removedVolume: string }>;
|
|
125
|
+
/** Poll until the upgrade is no longer `running` (default every 2 s, for up to 2 h). */
|
|
126
|
+
wait: (id: number, upgradeId: number, opts?: { intervalMs?: number; timeoutMs?: number }) => Promise<DatabaseUpgrade>;
|
|
127
|
+
};
|
|
107
128
|
/**
|
|
108
129
|
* 0.14: public access through a per-database Traefik TCP sidecar. Reading
|
|
109
130
|
* needs `admin` on the database; `set` and `disable` are operator-only.
|
|
@@ -224,6 +245,24 @@ export function databasesApi(t: Transport): DatabasesApi {
|
|
|
224
245
|
recoveries: (id) => get<PitrRecovery[]>(`/v1/databases/${id}/pitr/recoveries`),
|
|
225
246
|
recovery: (id, recoveryId) => get<PitrRecovery>(`/v1/databases/${id}/pitr/recoveries/${recoveryId}`),
|
|
226
247
|
},
|
|
248
|
+
upgrades: {
|
|
249
|
+
list: (id) => get<DatabaseUpgrade[]>(`/v1/databases/${id}/upgrades`),
|
|
250
|
+
options: (id) => get<DatabaseUpgradeOptions>(`/v1/databases/${id}/upgrades/options`),
|
|
251
|
+
start: (id, input) => send<DatabaseUpgrade>('POST', `/v1/databases/${id}/upgrades`, input),
|
|
252
|
+
rollback: (id, upgradeId) => send<DatabaseUpgrade>('POST', `/v1/databases/${id}/upgrades/${upgradeId}/rollback`, {}),
|
|
253
|
+
finalize: (id, upgradeId) => send<DatabaseUpgrade & { removedVolume: string }>('POST', `/v1/databases/${id}/upgrades/${upgradeId}/finalize`, {}),
|
|
254
|
+
wait: async (id, upgradeId, opts = {}) => {
|
|
255
|
+
const interval = opts.intervalMs ?? 2_000;
|
|
256
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 2 * 60 * 60 * 1000);
|
|
257
|
+
for (;;) {
|
|
258
|
+
const row = (await get<DatabaseUpgrade[]>(`/v1/databases/${id}/upgrades`)).find((u) => u.id === upgradeId);
|
|
259
|
+
if (!row) throw new Error(`upgrade ${upgradeId} of database ${id} not found`);
|
|
260
|
+
if (row.status !== 'running') return row;
|
|
261
|
+
if (Date.now() >= deadline) throw new Error(`upgrade ${upgradeId} is still running (phase ${row.phase ?? 'unknown'})`);
|
|
262
|
+
await new Promise<void>((resolve) => (globalThis as unknown as { setTimeout: (fn: () => void, ms: number) => unknown }).setTimeout(resolve, interval));
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
},
|
|
227
266
|
pgbouncerStatus: (id) => get<PgbouncerStatus>(`/v1/databases/${id}/pgbouncer`),
|
|
228
267
|
enablePgbouncer: (id, input) =>
|
|
229
268
|
send<PgbouncerStatus>('POST', `/v1/databases/${id}/pgbouncer/enable`, input ?? {}),
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
SizingReport,
|
|
8
8
|
RestartLimitConfig,
|
|
9
9
|
RestartLimitView,
|
|
10
|
+
ServiceCapabilitiesView,
|
|
10
11
|
PendingChangesView,
|
|
11
12
|
PendingApplyInput,
|
|
12
13
|
PendingApplyResult,
|
|
@@ -270,6 +271,8 @@ export interface ServicesApi {
|
|
|
270
271
|
enable: (id: number) => Promise<RegistryHookView>;
|
|
271
272
|
disable: (id: number) => Promise<RegistryHookView>;
|
|
272
273
|
};
|
|
274
|
+
/** C1: what the service can use where it runs, and why not (viewer). */
|
|
275
|
+
capabilities: (id: number) => Promise<ServiceCapabilitiesView>;
|
|
273
276
|
/** G1.3: stop the service when its containers keep restarting (opt-in; member to set). */
|
|
274
277
|
restartLimit: {
|
|
275
278
|
get: (id: number) => Promise<RestartLimitView>;
|
|
@@ -471,6 +474,7 @@ export function servicesApi(t: Transport): ServicesApi {
|
|
|
471
474
|
enable: (id) => send<RegistryHookView>('POST', `/v1/services/${id}/registry-hook`),
|
|
472
475
|
disable: (id) => request<RegistryHookView>(`/v1/services/${id}/registry-hook`, { method: 'DELETE' }),
|
|
473
476
|
},
|
|
477
|
+
capabilities: (id) => get<ServiceCapabilitiesView>(`/v1/services/${id}/capabilities`),
|
|
474
478
|
restartLimit: {
|
|
475
479
|
get: (id) => get<RestartLimitView>(`/v1/services/${id}/restart-limit`),
|
|
476
480
|
set: (id, limit) => send<RestartLimitView>('PUT', `/v1/services/${id}/restart-limit`, { limit }),
|
|
@@ -17,6 +17,8 @@ import type {
|
|
|
17
17
|
ExecProviderPut,
|
|
18
18
|
McpHttpPolicyPatch,
|
|
19
19
|
McpHttpPolicyView,
|
|
20
|
+
PanelAccessUpdate,
|
|
21
|
+
PanelAccessView,
|
|
20
22
|
} from '@ninedeploy/schemas';
|
|
21
23
|
import type { VaultAllowlistView, VaultSettings, EmailTemplateName, EmailTemplateEntry, EmailTemplateRender } from '../types.js';
|
|
22
24
|
import type { Transport } from '../transport.js';
|
|
@@ -84,6 +86,14 @@ export interface SettingsApi {
|
|
|
84
86
|
setAllowRegistration: (enabled: boolean) => Promise<{ ok: boolean; allowRegistration: boolean }>;
|
|
85
87
|
setAcmeEmail: (email: string) => Promise<{ ok: boolean; acmeEmail: string | null; applied: string }>;
|
|
86
88
|
setTemplatesSource: (source: string) => Promise<{ ok: boolean; templatesSource: string | null }>;
|
|
89
|
+
/**
|
|
90
|
+
* O4: the panel IP allow-list (operator, interactive session). `set`
|
|
91
|
+
* is refused when the list would exclude the caller's own address.
|
|
92
|
+
*/
|
|
93
|
+
panelAccess: {
|
|
94
|
+
get: () => Promise<PanelAccessView>;
|
|
95
|
+
set: (input: PanelAccessUpdate) => Promise<PanelAccessView>;
|
|
96
|
+
};
|
|
87
97
|
/** Configure the ACME DNS-01 challenge (wildcard certificates). */
|
|
88
98
|
setDns: (input: { provider: string; token?: string; wildcardApex: string }) => Promise<{ ok: boolean; dnsProvider: string | null; wildcardApex: string | null; applied: string }>;
|
|
89
99
|
/**
|
|
@@ -266,6 +276,10 @@ export function settingsApi(t: Transport): SettingsApi {
|
|
|
266
276
|
send<{ ok: boolean; acmeEmail: string | null; applied: string }>('PUT', '/v1/settings/acme-email', { email }),
|
|
267
277
|
setTemplatesSource: (source) =>
|
|
268
278
|
send<{ ok: boolean; templatesSource: string | null }>('PUT', '/v1/settings/templates-source', { source }),
|
|
279
|
+
panelAccess: {
|
|
280
|
+
get: () => get<PanelAccessView>('/v1/settings/panel-access'),
|
|
281
|
+
set: (input) => send<PanelAccessView>('PUT', '/v1/settings/panel-access', input),
|
|
282
|
+
},
|
|
269
283
|
setDns: (input) =>
|
|
270
284
|
send<{ ok: boolean; dnsProvider: string | null; wildcardApex: string | null; applied: string }>('PUT', '/v1/settings/dns', input),
|
|
271
285
|
autoUrl: {
|
package/src/resources/system.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
StatsSnapshot,
|
|
18
18
|
TopologyGraph,
|
|
19
19
|
UpdateCheckResult,
|
|
20
|
+
UpdateChannel,
|
|
20
21
|
SelfUpdateStatus,
|
|
21
22
|
AutoPruneConfigUpdateInput,
|
|
22
23
|
AutoPruneStatus,
|
|
@@ -55,6 +56,11 @@ export interface SystemApi {
|
|
|
55
56
|
exportUrl: () => string;
|
|
56
57
|
/** Latest-release check (admin). updateAvailable is null when offline/disabled. */
|
|
57
58
|
updateCheck: (force?: boolean) => Promise<UpdateCheckResult>;
|
|
59
|
+
/** A6: which releases the panel offers — every release, or the stable channel (operator). */
|
|
60
|
+
updateChannel: {
|
|
61
|
+
get: () => Promise<{ channel: UpdateChannel }>;
|
|
62
|
+
set: (channel: UpdateChannel) => Promise<{ channel: UpdateChannel; check: UpdateCheckResult }>;
|
|
63
|
+
};
|
|
58
64
|
/** State of a panel-initiated upgrade (idle/running/success/failed/unsupported). */
|
|
59
65
|
updateStatus: () => Promise<SelfUpdateStatus>;
|
|
60
66
|
/**
|
|
@@ -214,6 +220,10 @@ export function systemApi(t: Transport): SystemApi {
|
|
|
214
220
|
resources: () => get<DockerResources>('/v1/system/resources'),
|
|
215
221
|
pruneImages: () => send<{ ok: boolean }>('POST', '/v1/system/prune-images'),
|
|
216
222
|
updateCheck: (force) => get<UpdateCheckResult>(`/v1/system/update-check${force ? '?force=1' : ''}`),
|
|
223
|
+
updateChannel: {
|
|
224
|
+
get: () => get<{ channel: UpdateChannel }>('/v1/system/update-channel'),
|
|
225
|
+
set: (channel) => send<{ channel: UpdateChannel; check: UpdateCheckResult }>('PUT', '/v1/system/update-channel', { channel }),
|
|
226
|
+
},
|
|
217
227
|
updateStatus: () => get<SelfUpdateStatus>('/v1/system/update-status'),
|
|
218
228
|
updateStart: (version, opts) => send<{ ok: boolean }>('POST', '/v1/system/update-start', { version, ...opts }),
|
|
219
229
|
exportUrl: () => '/v1/system/export',
|
package/src/resources/volumes.ts
CHANGED
|
@@ -9,7 +9,9 @@ import type {
|
|
|
9
9
|
VolumeFileWriteInput,
|
|
10
10
|
VolumePathCreateInput,
|
|
11
11
|
VolumeCreateInput,
|
|
12
|
+
VolumeImport,
|
|
12
13
|
} from '@ninedeploy/schemas';
|
|
14
|
+
import { createVolumeImportHelpers, type ImportFileData, type ImportWaitOptions, type VolumeImportFileOptions } from '../databaseImport.js';
|
|
13
15
|
import { apiGetUrl } from '../apiPath.js';
|
|
14
16
|
import type { VolumeListEntry, VolumeCreateResult, ContainerInspectData, ContainerComposeData } from '../types.js';
|
|
15
17
|
import type { Transport } from '../transport.js';
|
|
@@ -78,10 +80,28 @@ export interface VolumesApi {
|
|
|
78
80
|
input: { filePath: string; attachmentId?: number; volumeName?: string },
|
|
79
81
|
) => Promise<{ ok: boolean; deploymentId: number }>;
|
|
80
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* A1: restore a gzipped tar of another host's volume into a service's data
|
|
85
|
+
* volume (`nd-svc-<slug>-data`). Operator only; docker services on the
|
|
86
|
+
* panel host with a volume mount. Uploads in ordered chunks with the
|
|
87
|
+
* archive's sha256; `start` is refused while the service runs, and a volume
|
|
88
|
+
* that exists is snapshotted first (a `pre-import` volume backup). A panel
|
|
89
|
+
* older than this feature answers 404.
|
|
90
|
+
*/
|
|
91
|
+
volumeImports: {
|
|
92
|
+
list: (serviceId: number) => Promise<VolumeImport[]>;
|
|
93
|
+
get: (serviceId: number, importId: number) => Promise<VolumeImport>;
|
|
94
|
+
cancel: (serviceId: number, importId: number) => Promise<VolumeImport>;
|
|
95
|
+
start: (serviceId: number, importId: number) => Promise<VolumeImport>;
|
|
96
|
+
wait: (serviceId: number, importId: number, options?: ImportWaitOptions<VolumeImport>) => Promise<VolumeImport>;
|
|
97
|
+
/** Create (or resume), upload, start and optionally poll, like `databases.importFile`. */
|
|
98
|
+
importFile: (serviceId: number, data: ImportFileData, options?: VolumeImportFileOptions) => Promise<VolumeImport>;
|
|
99
|
+
};
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
export function volumesApi(t: Transport): VolumesApi {
|
|
84
103
|
const { get, send, request } = t;
|
|
104
|
+
const vi = createVolumeImportHelpers(t);
|
|
85
105
|
const volumePath = (name: string, suffix: string, options?: { serverId?: number }) => {
|
|
86
106
|
const id = options?.serverId;
|
|
87
107
|
if (id != null && (!Number.isSafeInteger(id) || id < 1)) throw new TypeError('Invalid volume node id');
|
|
@@ -148,5 +168,13 @@ export function volumesApi(t: Transport): VolumesApi {
|
|
|
148
168
|
repairConfig: (serviceId, input) =>
|
|
149
169
|
send<{ ok: boolean; deploymentId: number }>('POST', `/v1/services/${serviceId}/volumes/config-repair`, input),
|
|
150
170
|
},
|
|
171
|
+
volumeImports: {
|
|
172
|
+
list: (serviceId) => get<VolumeImport[]>(vi.importPath(serviceId)),
|
|
173
|
+
get: vi.getImport,
|
|
174
|
+
cancel: (serviceId, importId) => send<VolumeImport>('DELETE', vi.importPath(serviceId, importId)),
|
|
175
|
+
start: vi.startImport,
|
|
176
|
+
wait: vi.waitForImport,
|
|
177
|
+
importFile: vi.importFile,
|
|
178
|
+
},
|
|
151
179
|
};
|
|
152
180
|
}
|