@lossless.org/objectstorage 10.7.1 → 11.1.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/.smartconfig.json +30 -10
- package/dist_rust/ruststorage_linux_amd64 +0 -0
- package/dist_rust/ruststorage_linux_amd64.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_linux_amd64_musl +0 -0
- package/dist_rust/ruststorage_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_linux_arm64 +0 -0
- package/dist_rust/ruststorage_linux_arm64.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_linux_arm64_musl +0 -0
- package/dist_rust/ruststorage_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_macos_amd64 +0 -0
- package/dist_rust/ruststorage_macos_amd64.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_macos_arm64 +0 -0
- package/dist_rust/ruststorage_macos_arm64.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dashboard.d.ts +49 -0
- package/dist_ts/classes.dashboard.js +234 -0
- package/dist_ts/dashboard.bundled.d.ts +4 -0
- package/dist_ts/dashboard.bundled.js +20 -0
- package/dist_ts/dashboard.interfaces.d.ts +132 -0
- package/dist_ts/dashboard.interfaces.js +2 -0
- package/dist_ts/dashboard.plugins.d.ts +5 -0
- package/dist_ts/dashboard.plugins.js +10 -0
- package/dist_ts/index.d.ts +219 -1
- package/dist_ts/index.js +209 -8
- package/package.json +17 -16
- package/readme.md +711 -30
- package/third-party-notices.md +21 -5
- package/third_party_licenses/borsh-1.8.1-license.txt +23 -0
- package/third_party_licenses/borsh-derive-1.8.1-apache-2.0.txt +201 -0
- package/third_party_licenses/cargo-dependencies.html +420 -0
- package/third_party_licenses/dyn-clone-1.0.20-license.txt +23 -0
- package/third_party_licenses/equivalent-1.0.3-license.txt +25 -0
- package/third_party_licenses/hashbrown-0.17.1-license.txt +25 -0
- package/third_party_licenses/indexmap-2.14.2-license.txt +25 -0
- package/third_party_licenses/inventory.json +310 -8
- package/third_party_licenses/proc-macro-crate-3.5.0-license.txt +23 -0
- package/third_party_licenses/ref-cast-1.0.27-license.txt +23 -0
- package/third_party_licenses/ref-cast-impl-1.0.27-license.txt +23 -0
- package/third_party_licenses/schemars-1.2.3-license.txt +21 -0
- package/third_party_licenses/syn-3.0.9-license.txt +23 -0
- package/third_party_licenses/toml_datetime-1.1.2-license.txt +19 -0
- package/third_party_licenses/toml_edit-0.25.17-license.txt +19 -0
- package/third_party_licenses/toml_parser-1.1.5-license.txt +19 -0
- package/third_party_licenses/winnow-1.0.4-license.txt +18 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dashboard.ts +280 -0
- package/ts/dashboard.bundled.ts.third-party-notices.json +1741 -0
- package/ts/dashboard.bundled.ts.third-party-notices.txt +5015 -0
- package/ts/dashboard.interfaces.ts +148 -0
- package/ts/dashboard.plugins.ts +13 -0
- package/ts/index.ts +473 -14
package/ts/index.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import * as paths from './paths.js';
|
|
3
|
+
import type { ObjectStorageDashboard } from './classes.dashboard.js';
|
|
4
|
+
import type {
|
|
5
|
+
IObjectStorageClusterSnapshot,
|
|
6
|
+
IObjectStorageDashboardConfig,
|
|
7
|
+
} from './dashboard.interfaces.js';
|
|
8
|
+
|
|
9
|
+
export * from './dashboard.interfaces.js';
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Authentication configuration
|
|
@@ -62,6 +69,11 @@ export interface ILimitsConfig {
|
|
|
62
69
|
export interface IMultipartConfig {
|
|
63
70
|
expirationDays?: number;
|
|
64
71
|
cleanupIntervalMinutes?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Open multipart uploads that the prefix credentials of one scope (bucket
|
|
74
|
+
* and prefix) may hold together (default 100). Counted in memory.
|
|
75
|
+
*/
|
|
76
|
+
maxUploadsPerPrefixScope?: number;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
/**
|
|
@@ -118,6 +130,11 @@ export interface IStorageConfig {
|
|
|
118
130
|
export interface IErasureConfig {
|
|
119
131
|
dataShards?: number;
|
|
120
132
|
parityShards?: number;
|
|
133
|
+
/**
|
|
134
|
+
* Bytes per erasure-coded chunk, between 1 and 64 MiB (67,108,864 bytes);
|
|
135
|
+
* startup refuses a value outside that range, because every manifest
|
|
136
|
+
* refuses a larger chunk. Default 4,194,304 (4 MiB).
|
|
137
|
+
*/
|
|
121
138
|
chunkSizeBytes?: number;
|
|
122
139
|
}
|
|
123
140
|
|
|
@@ -126,6 +143,31 @@ export interface IErasureConfig {
|
|
|
126
143
|
*/
|
|
127
144
|
export interface IDriveConfig {
|
|
128
145
|
paths: string[];
|
|
146
|
+
/**
|
|
147
|
+
* Accept drive roots that share a device (`st_dev`). Each drive root counts
|
|
148
|
+
* as one failure domain, so by default startup refuses two drive roots on
|
|
149
|
+
* one device. Set this only for dev and test clusters whose drive roots are
|
|
150
|
+
* directories on one filesystem. Default false.
|
|
151
|
+
*/
|
|
152
|
+
allowSharedDevice?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Mutual TLS for the cluster transport. Every file is read once at startup and
|
|
157
|
+
* must lie outside the control and drive roots, at an absolute path without
|
|
158
|
+
* symbolic links; a restart reloads them.
|
|
159
|
+
*/
|
|
160
|
+
export interface IClusterTlsConfig {
|
|
161
|
+
/** PEM file with one or two CA certificates (two only during a CA rotation). */
|
|
162
|
+
caFile: string;
|
|
163
|
+
/**
|
|
164
|
+
* PEM file with this node's certificate, optionally followed by intermediates. It has one DNS SAN,
|
|
165
|
+
* equal to nodeId, and is valid for server and client authentication: an extended key usage
|
|
166
|
+
* extension listing serverAuth and clientAuth, or no extended key usage extension.
|
|
167
|
+
*/
|
|
168
|
+
certFile: string;
|
|
169
|
+
/** PEM private key of certFile; owned by the engine's user, mode 0600 or 0400. */
|
|
170
|
+
keyFile: string;
|
|
129
171
|
}
|
|
130
172
|
|
|
131
173
|
/**
|
|
@@ -133,6 +175,11 @@ export interface IDriveConfig {
|
|
|
133
175
|
*/
|
|
134
176
|
export interface IClusterConfig {
|
|
135
177
|
enabled: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* This node's id. Generated and persisted when omitted, except with `tls`:
|
|
180
|
+
* then it is required and must be a lowercase DNS name equal to the node
|
|
181
|
+
* certificate's DNS subject alternative name.
|
|
182
|
+
*/
|
|
136
183
|
nodeId?: string;
|
|
137
184
|
quicPort?: number;
|
|
138
185
|
seedNodes?: string[];
|
|
@@ -140,12 +187,30 @@ export interface IClusterConfig {
|
|
|
140
187
|
drives?: IDriveConfig;
|
|
141
188
|
heartbeatIntervalMs?: number;
|
|
142
189
|
heartbeatTimeoutMs?: number;
|
|
190
|
+
/**
|
|
191
|
+
* Longest a single GET may run, in milliseconds, from its start to its last
|
|
192
|
+
* byte, including the client's transfer time. A longer GET is aborted.
|
|
193
|
+
* Superseded and deleted object generations keep their shards for this
|
|
194
|
+
* deadline plus the smaller of the deadline and one hour, then cleanup frees
|
|
195
|
+
* them. Must be the same on every node: a mismatch can abort streams but
|
|
196
|
+
* never serves wrong bytes. Default 21,600,000 (6 hours); allowed from
|
|
197
|
+
* 60,000 (1 minute) to 604,800,000 (7 days).
|
|
198
|
+
*/
|
|
199
|
+
getDeadlineMs?: number;
|
|
200
|
+
/**
|
|
201
|
+
* Mutual TLS for the cluster transport. Without it the transport is
|
|
202
|
+
* encrypted but authenticates no peer. Every node of a cluster sets it, or
|
|
203
|
+
* none does: enabling it is a full stop of every node, not a rolling change.
|
|
204
|
+
*/
|
|
205
|
+
tls?: IClusterTlsConfig;
|
|
143
206
|
}
|
|
144
207
|
|
|
145
208
|
/**
|
|
146
209
|
* Configuration as the engine receives it: every section resolved.
|
|
147
210
|
*/
|
|
148
|
-
export type TObjectStorageResolvedConfig = Required<
|
|
211
|
+
export type TObjectStorageResolvedConfig = Required<
|
|
212
|
+
Omit<IObjectStorageConfig, 'binaryPath' | 'dashboard'>
|
|
213
|
+
>;
|
|
149
214
|
|
|
150
215
|
/**
|
|
151
216
|
* Complete objectstorage configuration
|
|
@@ -166,6 +231,11 @@ export interface IObjectStorageConfig {
|
|
|
166
231
|
limits?: ILimitsConfig;
|
|
167
232
|
multipart?: IMultipartConfig;
|
|
168
233
|
cluster?: IClusterConfig;
|
|
234
|
+
/**
|
|
235
|
+
* The read-only visibility dashboard, served by this process on its own
|
|
236
|
+
* port. Off unless `enabled` is true; never sent to the engine.
|
|
237
|
+
*/
|
|
238
|
+
dashboard?: IObjectStorageDashboardConfig;
|
|
169
239
|
}
|
|
170
240
|
|
|
171
241
|
/**
|
|
@@ -341,6 +411,35 @@ export interface IBucketTenantMetadata {
|
|
|
341
411
|
retention?: IBucketRetentionReceipt;
|
|
342
412
|
}
|
|
343
413
|
|
|
414
|
+
export interface IPrefixCredentialInput {
|
|
415
|
+
bucketName: string;
|
|
416
|
+
/** Unreserved characters (RFC 3986) and `/`; ends with `/`, no leading `/`. */
|
|
417
|
+
prefix: string;
|
|
418
|
+
/** First node: lifetime in seconds, 1..43200. */
|
|
419
|
+
ttlSeconds?: number;
|
|
420
|
+
/** Fan-out to further nodes: the first node's expiresAt (epoch ms). */
|
|
421
|
+
expiresAt?: number;
|
|
422
|
+
/** Supply both together with expiresAt to install an existing credential on another node. */
|
|
423
|
+
accessKeyId?: string;
|
|
424
|
+
secretAccessKey?: string;
|
|
425
|
+
region?: string;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface IPrefixCredentialMetadata {
|
|
429
|
+
accessKeyId: string;
|
|
430
|
+
bucketName: string;
|
|
431
|
+
prefix: string;
|
|
432
|
+
region: string;
|
|
433
|
+
/** Epoch ms. */
|
|
434
|
+
createdAt: number;
|
|
435
|
+
/** Epoch ms; requests are refused from this instant and admitted ones end at it. */
|
|
436
|
+
expiresAt: number;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export interface IPrefixCredential extends IPrefixCredentialMetadata {
|
|
440
|
+
secretAccessKey: string;
|
|
441
|
+
}
|
|
442
|
+
|
|
344
443
|
export interface IBucketRetentionPolicyInput {
|
|
345
444
|
mode: 'compliance';
|
|
346
445
|
policyId: string;
|
|
@@ -998,6 +1097,10 @@ export interface IObjectStorageHealth {
|
|
|
998
1097
|
enabled: boolean;
|
|
999
1098
|
credentialCount: number;
|
|
1000
1099
|
tenantCredentialCount: number;
|
|
1100
|
+
prefixCredentialCount: number;
|
|
1101
|
+
prefixCredentialTombstoneCount: number;
|
|
1102
|
+
/** False when the prefix credential file is unreadable: every prefix credential is refused. */
|
|
1103
|
+
prefixCredentialStoreAvailable: boolean;
|
|
1001
1104
|
};
|
|
1002
1105
|
bucketCount: number;
|
|
1003
1106
|
objectCount: number;
|
|
@@ -1062,6 +1165,55 @@ const normalizeResourceFenceError = (errorArg: unknown): Error => {
|
|
|
1062
1165
|
);
|
|
1063
1166
|
};
|
|
1064
1167
|
|
|
1168
|
+
export const objectStorageCredentialErrorCodes = [
|
|
1169
|
+
'ECREDENTIAL_AUTH_DISABLED',
|
|
1170
|
+
'ECREDENTIAL_INVALID_PREFIX',
|
|
1171
|
+
'ECREDENTIAL_INVALID_TTL',
|
|
1172
|
+
'ECREDENTIAL_INVALID_EXPIRY',
|
|
1173
|
+
'ECREDENTIAL_INVALID_KEY',
|
|
1174
|
+
'ECREDENTIAL_NO_SUCH_BUCKET',
|
|
1175
|
+
'ECREDENTIAL_BUCKET_UNAVAILABLE',
|
|
1176
|
+
'ECREDENTIAL_CONFLICT',
|
|
1177
|
+
'ECREDENTIAL_REVOKED',
|
|
1178
|
+
'ECREDENTIAL_NOT_PREFIX',
|
|
1179
|
+
'ECREDENTIAL_LIMIT',
|
|
1180
|
+
'ECREDENTIAL_PERSISTENCE',
|
|
1181
|
+
'ECREDENTIAL_STORE_UNAVAILABLE',
|
|
1182
|
+
] as const;
|
|
1183
|
+
|
|
1184
|
+
export type TObjectStorageCredentialErrorCode =
|
|
1185
|
+
(typeof objectStorageCredentialErrorCodes)[number];
|
|
1186
|
+
|
|
1187
|
+
export class ObjectStorageCredentialError extends Error {
|
|
1188
|
+
public readonly code: TObjectStorageCredentialErrorCode;
|
|
1189
|
+
|
|
1190
|
+
public constructor(
|
|
1191
|
+
codeArg: TObjectStorageCredentialErrorCode,
|
|
1192
|
+
messageArg: string,
|
|
1193
|
+
causeArg?: unknown,
|
|
1194
|
+
) {
|
|
1195
|
+
super(`${codeArg}: ${messageArg}`, { cause: causeArg });
|
|
1196
|
+
this.name = 'ObjectStorageCredentialError';
|
|
1197
|
+
this.code = codeArg;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const normalizeCredentialError = (errorArg: unknown): Error => {
|
|
1202
|
+
const error = errorArg instanceof Error ? errorArg : new Error(String(errorArg));
|
|
1203
|
+
const match = /(ECREDENTIAL_[A-Z_]+):\s*(.*)$/s.exec(error.message);
|
|
1204
|
+
if (
|
|
1205
|
+
!match ||
|
|
1206
|
+
!objectStorageCredentialErrorCodes.includes(match[1] as TObjectStorageCredentialErrorCode)
|
|
1207
|
+
) {
|
|
1208
|
+
return error;
|
|
1209
|
+
}
|
|
1210
|
+
return new ObjectStorageCredentialError(
|
|
1211
|
+
match[1] as TObjectStorageCredentialErrorCode,
|
|
1212
|
+
match[2],
|
|
1213
|
+
error,
|
|
1214
|
+
);
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1065
1217
|
export interface IObjectStorageMetrics {
|
|
1066
1218
|
bucketCount: number;
|
|
1067
1219
|
objectCount: number;
|
|
@@ -1085,6 +1237,35 @@ export interface IClusterPeerHealth {
|
|
|
1085
1237
|
missedHeartbeats?: number;
|
|
1086
1238
|
}
|
|
1087
1239
|
|
|
1240
|
+
/**
|
|
1241
|
+
* A drive's medium: `'rotational'` and `'flash'` (network volumes included),
|
|
1242
|
+
* `'smr'` for drive-managed SMR, which reports itself as rotational and is
|
|
1243
|
+
* never detected, and `'zoned'` for host-managed or host-aware SMR and ZNS.
|
|
1244
|
+
*/
|
|
1245
|
+
export type TDriveMedia = 'rotational' | 'smr' | 'flash' | 'zoned';
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* A drive's media.
|
|
1249
|
+
*/
|
|
1250
|
+
export interface IClusterDriveMedia {
|
|
1251
|
+
/**
|
|
1252
|
+
* The media the configuration declares for the drive. Not reported yet:
|
|
1253
|
+
* drives cannot declare their media.
|
|
1254
|
+
*/
|
|
1255
|
+
declared?: TDriveMedia;
|
|
1256
|
+
/**
|
|
1257
|
+
* What the engine detected once at startup, from the whole disk behind the
|
|
1258
|
+
* drive root's filesystem (Linux sysfs `queue/zoned` and `queue/rotational`;
|
|
1259
|
+
* a partition counts as its disk). `'unknown'` for filesystems without a
|
|
1260
|
+
* block device of their own (tmpfs, overlay, network filesystems),
|
|
1261
|
+
* device-mapper, md, loop and other virtual block devices, and on every
|
|
1262
|
+
* platform but Linux. Virtual and network block devices report the
|
|
1263
|
+
* rotational flag their driver or hypervisor sets (QEMU disks show
|
|
1264
|
+
* `'rotational'` unless configured with `rotation_rate=1`).
|
|
1265
|
+
*/
|
|
1266
|
+
detected: TDriveMedia | 'unknown';
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1088
1269
|
/**
|
|
1089
1270
|
* Local drive health as measured by objectstorage's runtime probes.
|
|
1090
1271
|
*/
|
|
@@ -1099,6 +1280,7 @@ export interface IClusterDriveHealth {
|
|
|
1099
1280
|
lastError?: string;
|
|
1100
1281
|
lastCheck?: number;
|
|
1101
1282
|
erasureSetId?: number;
|
|
1283
|
+
media: IClusterDriveMedia;
|
|
1102
1284
|
}
|
|
1103
1285
|
|
|
1104
1286
|
export interface IClusterErasureHealth {
|
|
@@ -1119,10 +1301,31 @@ export interface IClusterRepairHealth {
|
|
|
1119
1301
|
lastDurationMs?: number;
|
|
1120
1302
|
shardsChecked?: number;
|
|
1121
1303
|
shardsHealed?: number;
|
|
1304
|
+
/** Pending shards (writes that failed at publication) the last scan left pending. */
|
|
1305
|
+
shardsPending?: number;
|
|
1122
1306
|
failed?: number;
|
|
1123
1307
|
lastError?: string;
|
|
1124
1308
|
}
|
|
1125
1309
|
|
|
1310
|
+
export interface IClusterTransportCertificate {
|
|
1311
|
+
/** SHA-256 of the DER certificate, lowercase hex. */
|
|
1312
|
+
sha256: string;
|
|
1313
|
+
/** Milliseconds since the Unix epoch. */
|
|
1314
|
+
notAfter: number;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* How the cluster transport authenticates peers, and the certificates it
|
|
1319
|
+
* loaded at startup.
|
|
1320
|
+
*/
|
|
1321
|
+
export interface IClusterTransportHealth {
|
|
1322
|
+
mode: 'mutualTls' | 'unauthenticated';
|
|
1323
|
+
/** This node's certificate; only in mode 'mutualTls'. */
|
|
1324
|
+
certificate?: IClusterTransportCertificate;
|
|
1325
|
+
/** The loaded CA bundle in file order; only in mode 'mutualTls'. */
|
|
1326
|
+
caCertificates?: IClusterTransportCertificate[];
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1126
1329
|
/**
|
|
1127
1330
|
* Cluster runtime health from the Rust core.
|
|
1128
1331
|
* When clustering is disabled, the response is `{ enabled: false }`.
|
|
@@ -1138,6 +1341,36 @@ export interface IClusterHealth {
|
|
|
1138
1341
|
drives?: IClusterDriveHealth[];
|
|
1139
1342
|
erasure?: IClusterErasureHealth;
|
|
1140
1343
|
repairs?: IClusterRepairHealth;
|
|
1344
|
+
transport?: IClusterTransportHealth;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* The result of one cluster healing scan, as `ObjectStorage.runHealingScan()`
|
|
1349
|
+
* returns it. Times are milliseconds since the Unix epoch.
|
|
1350
|
+
*/
|
|
1351
|
+
export interface IClusterHealingScanResult {
|
|
1352
|
+
/** When the scan started; always after the request arrived. */
|
|
1353
|
+
startedAt: number;
|
|
1354
|
+
completedAt: number;
|
|
1355
|
+
durationMs: number;
|
|
1356
|
+
/** Shard placements checked at their drives (placements on offline nodes are not). */
|
|
1357
|
+
shardsChecked: number;
|
|
1358
|
+
/** Shards written: missing placements rebuilt and pending shards completed. */
|
|
1359
|
+
shardsHealed: number;
|
|
1360
|
+
/** Pending shards (writes that failed at publication) this scan left pending. */
|
|
1361
|
+
shardsPending: number;
|
|
1362
|
+
/** Failed checks, rebuilds, writes and manifest stores. */
|
|
1363
|
+
failed: number;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
export interface IRunHealingScanOptions {
|
|
1367
|
+
/**
|
|
1368
|
+
* How long to wait for the scan, in milliseconds. Defaults to the bridge's
|
|
1369
|
+
* request timeout (300,000). The engine cannot cancel a scan: one that
|
|
1370
|
+
* outlasts this keeps running, its result is discarded, and a later call
|
|
1371
|
+
* joins the next scan that starts after it.
|
|
1372
|
+
*/
|
|
1373
|
+
timeoutMs?: number;
|
|
1141
1374
|
}
|
|
1142
1375
|
|
|
1143
1376
|
/**
|
|
@@ -1185,6 +1418,7 @@ const DEFAULT_CONFIG: IObjectStorageConfig = {
|
|
|
1185
1418
|
multipart: {
|
|
1186
1419
|
expirationDays: 7,
|
|
1187
1420
|
cleanupIntervalMinutes: 60,
|
|
1421
|
+
maxUploadsPerPrefixScope: 100,
|
|
1188
1422
|
},
|
|
1189
1423
|
};
|
|
1190
1424
|
|
|
@@ -1309,10 +1543,41 @@ function mergeConfig(userConfig: IObjectStorageConfig): TObjectStorageResolvedCo
|
|
|
1309
1543
|
} as TObjectStorageResolvedConfig;
|
|
1310
1544
|
}
|
|
1311
1545
|
|
|
1546
|
+
const DEFAULT_DASHBOARD_ADDRESS = '127.0.0.1';
|
|
1547
|
+
const DEFAULT_DASHBOARD_PORT = 3001;
|
|
1548
|
+
|
|
1549
|
+
function resolveDashboardConfig(
|
|
1550
|
+
dashboardArg: IObjectStorageDashboardConfig | undefined,
|
|
1551
|
+
s3PortArg: number,
|
|
1552
|
+
): Required<IObjectStorageDashboardConfig> | undefined {
|
|
1553
|
+
if (dashboardArg === undefined) {
|
|
1554
|
+
return undefined;
|
|
1555
|
+
}
|
|
1556
|
+
if (typeof dashboardArg.enabled !== 'boolean') {
|
|
1557
|
+
throw new Error('dashboard.enabled must be a boolean.');
|
|
1558
|
+
}
|
|
1559
|
+
const address = dashboardArg.address ?? DEFAULT_DASHBOARD_ADDRESS;
|
|
1560
|
+
if (typeof address !== 'string' || address.length === 0) {
|
|
1561
|
+
throw new Error('dashboard.address must be a non-empty string.');
|
|
1562
|
+
}
|
|
1563
|
+
const port = dashboardArg.port ?? DEFAULT_DASHBOARD_PORT;
|
|
1564
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
1565
|
+
throw new Error('dashboard.port must be an integer from 0 to 65535.');
|
|
1566
|
+
}
|
|
1567
|
+
if (dashboardArg.enabled && port !== 0 && port === s3PortArg) {
|
|
1568
|
+
throw new Error(`dashboard.port ${port} is the S3 port; the dashboard needs its own port.`);
|
|
1569
|
+
}
|
|
1570
|
+
return { enabled: dashboardArg.enabled, address, port };
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1312
1573
|
function createAccessKeyId(): string {
|
|
1313
1574
|
return `SS${plugins.crypto.randomBytes(10).toString('hex').toUpperCase()}`;
|
|
1314
1575
|
}
|
|
1315
1576
|
|
|
1577
|
+
function createPrefixAccessKeyId(): string {
|
|
1578
|
+
return `SP${plugins.crypto.randomBytes(10).toString('hex').toUpperCase()}`;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1316
1581
|
function createSecretAccessKey(): string {
|
|
1317
1582
|
return plugins.crypto.randomBytes(32).toString('hex');
|
|
1318
1583
|
}
|
|
@@ -1441,7 +1706,30 @@ type TRustStorageCommands = {
|
|
|
1441
1706
|
listBucketSummaries: { params: {}; result: IBucketSummary[] };
|
|
1442
1707
|
listCredentials: { params: {}; result: IStorageCredentialMetadata[] };
|
|
1443
1708
|
replaceCredentials: { params: { credentials: IStorageCredential[] }; result: {} };
|
|
1709
|
+
createPrefixCredential: {
|
|
1710
|
+
params: {
|
|
1711
|
+
bucketName: string;
|
|
1712
|
+
prefix: string;
|
|
1713
|
+
accessKeyId: string;
|
|
1714
|
+
secretAccessKey: string;
|
|
1715
|
+
region?: string;
|
|
1716
|
+
ttlSeconds?: number;
|
|
1717
|
+
expiresAt?: number;
|
|
1718
|
+
};
|
|
1719
|
+
result: IPrefixCredentialMetadata;
|
|
1720
|
+
};
|
|
1721
|
+
revokePrefixCredential: { params: { accessKeyId: string }; result: { revoked: boolean } };
|
|
1722
|
+
listPrefixCredentials: {
|
|
1723
|
+
params: { bucketName?: string };
|
|
1724
|
+
result: IPrefixCredentialMetadata[];
|
|
1725
|
+
};
|
|
1726
|
+
getPrefixCredentialStatus: {
|
|
1727
|
+
params: {};
|
|
1728
|
+
result: { available: boolean; credentialCount: number; tombstoneCount: number };
|
|
1729
|
+
};
|
|
1444
1730
|
getClusterHealth: { params: {}; result: IClusterHealth };
|
|
1731
|
+
getClusterSnapshot: { params: {}; result: IObjectStorageClusterSnapshot };
|
|
1732
|
+
runHealingScan: { params: {}; result: IClusterHealingScanResult };
|
|
1445
1733
|
};
|
|
1446
1734
|
|
|
1447
1735
|
/**
|
|
@@ -1525,6 +1813,8 @@ export class ObjectStorage {
|
|
|
1525
1813
|
private bridgeOwned = false;
|
|
1526
1814
|
private stopRequested = false;
|
|
1527
1815
|
private stopCompleted = false;
|
|
1816
|
+
private readonly dashboardConfig: Required<IObjectStorageDashboardConfig> | undefined;
|
|
1817
|
+
private dashboard: ObjectStorageDashboard | undefined;
|
|
1528
1818
|
|
|
1529
1819
|
constructor(configArg: IObjectStorageConfig = {}) {
|
|
1530
1820
|
const binaryPath = configArg.binaryPath;
|
|
@@ -1535,6 +1825,7 @@ export class ObjectStorage {
|
|
|
1535
1825
|
throw new Error('binaryPath must be a non-empty executable path.');
|
|
1536
1826
|
}
|
|
1537
1827
|
this.config = mergeConfig(configArg);
|
|
1828
|
+
this.dashboardConfig = resolveDashboardConfig(configArg.dashboard, this.config.server.port!);
|
|
1538
1829
|
this.bridge = new plugins.RustBridge<TRustStorageCommands>({
|
|
1539
1830
|
binaryName: 'ruststorage',
|
|
1540
1831
|
...(binaryPath !== undefined
|
|
@@ -1600,6 +1891,23 @@ export class ObjectStorage {
|
|
|
1600
1891
|
throw new Error('Storage engine did not report a valid bound TCP port.');
|
|
1601
1892
|
}
|
|
1602
1893
|
this.boundPort = result.port;
|
|
1894
|
+
if (this.dashboardConfig?.enabled) {
|
|
1895
|
+
// Enabled but unable to start (a port in use, say) fails startup:
|
|
1896
|
+
// the catch below stops the engine.
|
|
1897
|
+
const { ObjectStorageDashboard } = await import('./classes.dashboard.js');
|
|
1898
|
+
const dashboard = new ObjectStorageDashboard({
|
|
1899
|
+
address: this.dashboardConfig.address,
|
|
1900
|
+
port: this.dashboardConfig.port,
|
|
1901
|
+
readSnapshot: () => this.getClusterSnapshot(),
|
|
1902
|
+
});
|
|
1903
|
+
try {
|
|
1904
|
+
await dashboard.start();
|
|
1905
|
+
} catch (dashboardError) {
|
|
1906
|
+
await dashboard.stop();
|
|
1907
|
+
throw dashboardError;
|
|
1908
|
+
}
|
|
1909
|
+
this.dashboard = dashboard;
|
|
1910
|
+
}
|
|
1603
1911
|
} catch (error) {
|
|
1604
1912
|
try {
|
|
1605
1913
|
if (this.bridgeOwned) {
|
|
@@ -1627,6 +1935,11 @@ export class ObjectStorage {
|
|
|
1627
1935
|
return this.running ? this.boundPort : this.config.server.port!;
|
|
1628
1936
|
}
|
|
1629
1937
|
|
|
1938
|
+
/** The dashboard's bound TCP port while it runs; `undefined` otherwise. */
|
|
1939
|
+
public get dashboardPort(): number | undefined {
|
|
1940
|
+
return this.dashboard?.port;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1630
1943
|
public async getStorageDescriptor(
|
|
1631
1944
|
optionsArg?: Partial<plugins.tsclass.storage.IS3Descriptor>,
|
|
1632
1945
|
): Promise<plugins.tsclass.storage.IS3Descriptor> {
|
|
@@ -1730,7 +2043,7 @@ export class ObjectStorage {
|
|
|
1730
2043
|
});
|
|
1731
2044
|
return this.buildBucketTenantDescriptor(credential, tenantArg.bucketName);
|
|
1732
2045
|
} catch (error) {
|
|
1733
|
-
throw normalizeResourceFenceError(error);
|
|
2046
|
+
throw normalizeCredentialError(normalizeResourceFenceError(error));
|
|
1734
2047
|
}
|
|
1735
2048
|
}
|
|
1736
2049
|
|
|
@@ -1745,7 +2058,7 @@ export class ObjectStorage {
|
|
|
1745
2058
|
});
|
|
1746
2059
|
return this.buildBucketTenantDescriptor(credential, tenantArg.bucketName);
|
|
1747
2060
|
} catch (error) {
|
|
1748
|
-
throw normalizeResourceFenceError(error);
|
|
2061
|
+
throw normalizeCredentialError(normalizeResourceFenceError(error));
|
|
1749
2062
|
}
|
|
1750
2063
|
}
|
|
1751
2064
|
|
|
@@ -1754,7 +2067,7 @@ export class ObjectStorage {
|
|
|
1754
2067
|
try {
|
|
1755
2068
|
await this.bridge.sendCommand('deleteBucketTenant', tenantArg);
|
|
1756
2069
|
} catch (error) {
|
|
1757
|
-
throw normalizeResourceFenceError(error);
|
|
2070
|
+
throw normalizeCredentialError(normalizeResourceFenceError(error));
|
|
1758
2071
|
}
|
|
1759
2072
|
}
|
|
1760
2073
|
|
|
@@ -1771,7 +2084,7 @@ export class ObjectStorage {
|
|
|
1771
2084
|
});
|
|
1772
2085
|
return this.buildBucketTenantDescriptor(credential, tenantArg.bucketName);
|
|
1773
2086
|
} catch (error) {
|
|
1774
|
-
throw normalizeResourceFenceError(error);
|
|
2087
|
+
throw normalizeCredentialError(normalizeResourceFenceError(error));
|
|
1775
2088
|
}
|
|
1776
2089
|
}
|
|
1777
2090
|
|
|
@@ -1790,6 +2103,95 @@ export class ObjectStorage {
|
|
|
1790
2103
|
return this.buildBucketTenantDescriptor(credential, optionsArg.bucketName);
|
|
1791
2104
|
}
|
|
1792
2105
|
|
|
2106
|
+
/**
|
|
2107
|
+
* Creates a credential confined to one key prefix of one bucket until its
|
|
2108
|
+
* expiry. The first node takes `ttlSeconds`; further nodes of a fan-out take
|
|
2109
|
+
* the first result's `accessKeyId`, `secretAccessKey` and `expiresAt`. The
|
|
2110
|
+
* secret is returned only here and never kept by this instance.
|
|
2111
|
+
*/
|
|
2112
|
+
public async createPrefixCredential(
|
|
2113
|
+
inputArg: IPrefixCredentialInput,
|
|
2114
|
+
): Promise<IPrefixCredential> {
|
|
2115
|
+
const hasTtl = inputArg.ttlSeconds !== undefined;
|
|
2116
|
+
const hasExpiresAt = inputArg.expiresAt !== undefined;
|
|
2117
|
+
if (hasTtl === hasExpiresAt) {
|
|
2118
|
+
throw new ObjectStorageCredentialError(
|
|
2119
|
+
'ECREDENTIAL_INVALID_TTL',
|
|
2120
|
+
'Exactly one of ttlSeconds and expiresAt is required.',
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
if (hasTtl && (!Number.isSafeInteger(inputArg.ttlSeconds) || inputArg.ttlSeconds! < 1)) {
|
|
2124
|
+
throw new ObjectStorageCredentialError(
|
|
2125
|
+
'ECREDENTIAL_INVALID_TTL',
|
|
2126
|
+
'ttlSeconds must be an integer from 1 to 43200.',
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
if (hasExpiresAt && !Number.isSafeInteger(inputArg.expiresAt)) {
|
|
2130
|
+
throw new ObjectStorageCredentialError(
|
|
2131
|
+
'ECREDENTIAL_INVALID_EXPIRY',
|
|
2132
|
+
'expiresAt must be an integer epoch millisecond.',
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
const hasAccessKeyId = inputArg.accessKeyId !== undefined;
|
|
2136
|
+
const hasSecretAccessKey = inputArg.secretAccessKey !== undefined;
|
|
2137
|
+
if (hasAccessKeyId !== hasSecretAccessKey) {
|
|
2138
|
+
throw new ObjectStorageCredentialError(
|
|
2139
|
+
'ECREDENTIAL_INVALID_KEY',
|
|
2140
|
+
'accessKeyId and secretAccessKey must be supplied together.',
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
if (hasAccessKeyId && !hasExpiresAt) {
|
|
2144
|
+
throw new ObjectStorageCredentialError(
|
|
2145
|
+
'ECREDENTIAL_INVALID_KEY',
|
|
2146
|
+
'Key material is accepted only with expiresAt, to install an existing credential.',
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
const secretAccessKey = inputArg.secretAccessKey ?? createSecretAccessKey();
|
|
2150
|
+
try {
|
|
2151
|
+
const metadata = await this.bridge.sendCommand('createPrefixCredential', {
|
|
2152
|
+
bucketName: inputArg.bucketName,
|
|
2153
|
+
prefix: inputArg.prefix,
|
|
2154
|
+
accessKeyId: inputArg.accessKeyId ?? createPrefixAccessKeyId(),
|
|
2155
|
+
secretAccessKey,
|
|
2156
|
+
region: inputArg.region || this.config.server.region,
|
|
2157
|
+
...(hasTtl ? { ttlSeconds: inputArg.ttlSeconds } : { expiresAt: inputArg.expiresAt }),
|
|
2158
|
+
});
|
|
2159
|
+
return { ...metadata, secretAccessKey };
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
throw normalizeCredentialError(error);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
/**
|
|
2166
|
+
* Revokes a prefix credential on this node. An unknown ID is recorded as
|
|
2167
|
+
* revoked too, so a later create for it is refused; `revoked` is true only
|
|
2168
|
+
* when a live credential was removed.
|
|
2169
|
+
*/
|
|
2170
|
+
public async revokePrefixCredential(optionsArg: {
|
|
2171
|
+
accessKeyId: string;
|
|
2172
|
+
}): Promise<{ revoked: boolean }> {
|
|
2173
|
+
try {
|
|
2174
|
+
return await this.bridge.sendCommand('revokePrefixCredential', {
|
|
2175
|
+
accessKeyId: optionsArg.accessKeyId,
|
|
2176
|
+
});
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
throw normalizeCredentialError(error);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
/** Lists this node's live prefix credentials, without secrets. */
|
|
2183
|
+
public async listPrefixCredentials(
|
|
2184
|
+
optionsArg: { bucketName?: string } = {},
|
|
2185
|
+
): Promise<IPrefixCredentialMetadata[]> {
|
|
2186
|
+
try {
|
|
2187
|
+
return await this.bridge.sendCommand('listPrefixCredentials', {
|
|
2188
|
+
...(optionsArg.bucketName !== undefined ? { bucketName: optionsArg.bucketName } : {}),
|
|
2189
|
+
});
|
|
2190
|
+
} catch (error) {
|
|
2191
|
+
throw normalizeCredentialError(error);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
1793
2195
|
public async getBucketRetentionCapability(): Promise<IBucketRetentionCapability> {
|
|
1794
2196
|
return this.bridge.sendCommand('getBucketRetentionCapability', {});
|
|
1795
2197
|
}
|
|
@@ -1864,7 +2266,7 @@ export class ObjectStorage {
|
|
|
1864
2266
|
try {
|
|
1865
2267
|
return await this.bridge.sendCommand('deleteBucket', optionsArg);
|
|
1866
2268
|
} catch (error) {
|
|
1867
|
-
throw normalizeResourceFenceError(error);
|
|
2269
|
+
throw normalizeCredentialError(normalizeResourceFenceError(error));
|
|
1868
2270
|
}
|
|
1869
2271
|
}
|
|
1870
2272
|
|
|
@@ -1914,7 +2316,11 @@ export class ObjectStorage {
|
|
|
1914
2316
|
* if this override introduces ownership or credential-material drift.
|
|
1915
2317
|
*/
|
|
1916
2318
|
public async replaceCredentials(credentials: IStorageCredential[]): Promise<void> {
|
|
1917
|
-
|
|
2319
|
+
try {
|
|
2320
|
+
await this.bridge.sendCommand('replaceCredentials', { credentials });
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
throw normalizeCredentialError(error);
|
|
2323
|
+
}
|
|
1918
2324
|
this.config.auth.credentials = credentials.map((credential) => ({ ...credential }));
|
|
1919
2325
|
}
|
|
1920
2326
|
|
|
@@ -1922,6 +2328,38 @@ export class ObjectStorage {
|
|
|
1922
2328
|
return this.bridge.sendCommand('getClusterHealth', {});
|
|
1923
2329
|
}
|
|
1924
2330
|
|
|
2331
|
+
/**
|
|
2332
|
+
* The dashboard's snapshot, built by the engine from memory only: it never
|
|
2333
|
+
* probes a drive, syncs or walks storage. A cluster node also asks each
|
|
2334
|
+
* online peer for its own snapshot, waiting up to a second per peer, in
|
|
2335
|
+
* parallel; a peer that does not answer is reported unavailable. One
|
|
2336
|
+
* snapshot runs at a time: a call while another runs rejects.
|
|
2337
|
+
*/
|
|
2338
|
+
public async getClusterSnapshot(): Promise<IObjectStorageClusterSnapshot> {
|
|
2339
|
+
return this.bridge.sendCommand('getClusterSnapshot', {});
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
/**
|
|
2343
|
+
* Runs a healing scan on this cluster node and resolves with its result.
|
|
2344
|
+
* The scan covers the manifests this node coordinated. The request joins
|
|
2345
|
+
* the next scan that starts after it arrives, never one already running,
|
|
2346
|
+
* and requests that arrive together share one scan. It rejects in
|
|
2347
|
+
* standalone mode, when the scan fails (for example without a majority of
|
|
2348
|
+
* the cluster, or when a bucket's manifests cannot be listed), when 16
|
|
2349
|
+
* requests already wait for a scan to start, and when the engine shuts down
|
|
2350
|
+
* before the scan completes. The engine answers other management calls
|
|
2351
|
+
* while the scan runs.
|
|
2352
|
+
*/
|
|
2353
|
+
public async runHealingScan(
|
|
2354
|
+
optionsArg: IRunHealingScanOptions = {},
|
|
2355
|
+
): Promise<IClusterHealingScanResult> {
|
|
2356
|
+
return this.bridge.sendCommand(
|
|
2357
|
+
'runHealingScan',
|
|
2358
|
+
{},
|
|
2359
|
+
optionsArg.timeoutMs === undefined ? undefined : { timeoutMs: optionsArg.timeoutMs },
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
1925
2363
|
public async getHealth(): Promise<IObjectStorageHealth> {
|
|
1926
2364
|
if (!this.running) {
|
|
1927
2365
|
return {
|
|
@@ -1933,6 +2371,9 @@ export class ObjectStorage {
|
|
|
1933
2371
|
enabled: this.config.auth.enabled,
|
|
1934
2372
|
credentialCount: this.config.auth.credentials.length,
|
|
1935
2373
|
tenantCredentialCount: 0,
|
|
2374
|
+
prefixCredentialCount: 0,
|
|
2375
|
+
prefixCredentialTombstoneCount: 0,
|
|
2376
|
+
prefixCredentialStoreAvailable: false,
|
|
1936
2377
|
},
|
|
1937
2378
|
bucketCount: 0,
|
|
1938
2379
|
objectCount: 0,
|
|
@@ -1947,13 +2388,15 @@ export class ObjectStorage {
|
|
|
1947
2388
|
};
|
|
1948
2389
|
}
|
|
1949
2390
|
|
|
1950
|
-
const [stats, credentials, tenants, cluster, resourceFencing] =
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2391
|
+
const [stats, credentials, tenants, prefixCredentials, cluster, resourceFencing] =
|
|
2392
|
+
await Promise.all([
|
|
2393
|
+
this.getStorageStats(),
|
|
2394
|
+
this.listCredentials(),
|
|
2395
|
+
this.config.auth.enabled ? this.listBucketTenants() : Promise.resolve([]),
|
|
2396
|
+
this.bridge.sendCommand('getPrefixCredentialStatus', {}),
|
|
2397
|
+
this.getClusterHealth(),
|
|
2398
|
+
this.getResourceFencingCapability(),
|
|
2399
|
+
]);
|
|
1957
2400
|
return {
|
|
1958
2401
|
ok: true,
|
|
1959
2402
|
running: true,
|
|
@@ -1963,6 +2406,9 @@ export class ObjectStorage {
|
|
|
1963
2406
|
enabled: this.config.auth.enabled,
|
|
1964
2407
|
credentialCount: credentials.length,
|
|
1965
2408
|
tenantCredentialCount: tenants.length,
|
|
2409
|
+
prefixCredentialCount: prefixCredentials.credentialCount,
|
|
2410
|
+
prefixCredentialTombstoneCount: prefixCredentials.tombstoneCount,
|
|
2411
|
+
prefixCredentialStoreAvailable: prefixCredentials.available,
|
|
1966
2412
|
},
|
|
1967
2413
|
bucketCount: stats.bucketCount,
|
|
1968
2414
|
objectCount: stats.totalObjectCount,
|
|
@@ -2002,6 +2448,15 @@ export class ObjectStorage {
|
|
|
2002
2448
|
'# HELP objectstorage_tenant_credentials_total Scoped bucket tenant credential count.',
|
|
2003
2449
|
'# TYPE objectstorage_tenant_credentials_total gauge',
|
|
2004
2450
|
`objectstorage_tenant_credentials_total ${health.auth.tenantCredentialCount}`,
|
|
2451
|
+
'# HELP objectstorage_prefix_credentials_total Live prefix credential count.',
|
|
2452
|
+
'# TYPE objectstorage_prefix_credentials_total gauge',
|
|
2453
|
+
`objectstorage_prefix_credentials_total ${health.auth.prefixCredentialCount}`,
|
|
2454
|
+
'# HELP objectstorage_prefix_credential_tombstones_total Unexpired prefix credential revocations.',
|
|
2455
|
+
'# TYPE objectstorage_prefix_credential_tombstones_total gauge',
|
|
2456
|
+
`objectstorage_prefix_credential_tombstones_total ${health.auth.prefixCredentialTombstoneCount}`,
|
|
2457
|
+
'# HELP objectstorage_prefix_credential_store_available Prefix credential store readable.',
|
|
2458
|
+
'# TYPE objectstorage_prefix_credential_store_available gauge',
|
|
2459
|
+
`objectstorage_prefix_credential_store_available ${health.auth.prefixCredentialStoreAvailable ? 1 : 0}`,
|
|
2005
2460
|
'# HELP objectstorage_cluster_enabled Cluster mode enabled.',
|
|
2006
2461
|
'# TYPE objectstorage_cluster_enabled gauge',
|
|
2007
2462
|
`objectstorage_cluster_enabled ${clusterEnabled ? 1 : 0}`,
|
|
@@ -2034,6 +2489,10 @@ export class ObjectStorage {
|
|
|
2034
2489
|
// Startup keeps its own rejection; stop joins only its resource ownership.
|
|
2035
2490
|
await Promise.allSettled([this.startPromise]);
|
|
2036
2491
|
}
|
|
2492
|
+
if (this.dashboard) {
|
|
2493
|
+
await this.dashboard.stop();
|
|
2494
|
+
this.dashboard = undefined;
|
|
2495
|
+
}
|
|
2037
2496
|
if (this.bridgeOwned) {
|
|
2038
2497
|
if (this.running) {
|
|
2039
2498
|
this.completedStopResult ??= await this.bridge.sendCommand('stop', {});
|