@git.zone/cli 3.0.1 → 3.0.2
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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mod_docker/classes.dockerpruner.js +6 -7
- package/dist_ts/mod_release/index.js +3 -3
- package/dist_ts/mod_services/classes.dockercontainer.d.ts +37 -0
- package/dist_ts/mod_services/classes.dockercontainer.js +108 -6
- package/dist_ts/mod_services/classes.serviceconfiguration.d.ts +17 -0
- package/dist_ts/mod_services/classes.serviceconfiguration.js +90 -3
- package/dist_ts/mod_services/classes.servicemanager.d.ts +23 -0
- package/dist_ts/mod_services/classes.servicemanager.js +374 -89
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/package.json +5 -5
- package/readme.md +14 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mod_docker/classes.dockerpruner.ts +6 -7
- package/ts/mod_release/index.ts +2 -2
- package/ts/mod_services/classes.dockercontainer.ts +149 -5
- package/ts/mod_services/classes.serviceconfiguration.ts +110 -2
- package/ts/mod_services/classes.servicemanager.ts +550 -112
- package/ts/plugins.ts +2 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import * as plugins from './mod.plugins.js';
|
|
2
2
|
import * as helpers from './helpers.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ServiceConfiguration,
|
|
5
|
+
getS3BucketNameValidationError,
|
|
6
|
+
} from './classes.serviceconfiguration.js';
|
|
4
7
|
import {
|
|
5
8
|
DockerContainer,
|
|
6
9
|
matchDockerUlimits,
|
|
10
|
+
type IDockerExecArgvResult,
|
|
7
11
|
type TDockerUlimits,
|
|
8
12
|
} from './classes.dockercontainer.js';
|
|
9
13
|
import { GlobalRegistry } from './classes.globalregistry.js';
|
|
@@ -55,11 +59,254 @@ export const mongoServiceUlimits = {
|
|
|
55
59
|
},
|
|
56
60
|
} as const satisfies TDockerUlimits;
|
|
57
61
|
|
|
62
|
+
export const minioReconciliationTimeoutMs = 30_000;
|
|
63
|
+
const minioProbeTimeoutCapMs = 2_000;
|
|
64
|
+
const minioProbeIntervalMs = 250;
|
|
65
|
+
const minioRollbackTimeoutMs = 5_000;
|
|
66
|
+
const minioAlias = 'gitzone-local';
|
|
67
|
+
const minioAliasSetupScript =
|
|
68
|
+
'mc alias set gitzone-local http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null';
|
|
69
|
+
|
|
70
|
+
export interface IMinioReconciliationClock {
|
|
71
|
+
now(): number;
|
|
72
|
+
delay(millisecondsArg: number): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface IMinioReconciliationOptions {
|
|
76
|
+
docker: Pick<DockerContainer, 'execArgv'>;
|
|
77
|
+
/** Full immutable Docker container id; mutable names are never accepted here. */
|
|
78
|
+
containerId: string;
|
|
79
|
+
bucket: string;
|
|
80
|
+
/** Shared absolute deadline. Defaults to 30 seconds from this call. */
|
|
81
|
+
deadlineAt?: number;
|
|
82
|
+
clock?: IMinioReconciliationClock;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const defaultMinioClock: IMinioReconciliationClock = {
|
|
86
|
+
now: () => Date.now(),
|
|
87
|
+
delay: async (millisecondsArg) => plugins.smartdelay.delayFor(millisecondsArg),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
class MinioStageError extends Error {
|
|
91
|
+
constructor(stageArg: string, timedOutArg: boolean, causeArg?: unknown) {
|
|
92
|
+
const message = timedOutArg
|
|
93
|
+
? `MinIO ${stageArg} did not complete before the shared reconciliation deadline.`
|
|
94
|
+
: `MinIO ${stageArg} failed.`;
|
|
95
|
+
super(message, causeArg === undefined ? undefined : { cause: causeArg });
|
|
96
|
+
this.name = 'MinioStageError';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const minioStageError = (
|
|
101
|
+
stageArg: string,
|
|
102
|
+
timedOutArg: boolean,
|
|
103
|
+
causeArg?: unknown,
|
|
104
|
+
): MinioStageError => {
|
|
105
|
+
return new MinioStageError(stageArg, timedOutArg, causeArg);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const minioStageCause = (errorArg: unknown): unknown => {
|
|
109
|
+
if (errorArg instanceof MinioStageError && errorArg.cause !== undefined) {
|
|
110
|
+
return errorArg.cause;
|
|
111
|
+
}
|
|
112
|
+
return errorArg;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const normalizeMinioLifecycleError = (errorArg: unknown): Error => {
|
|
116
|
+
return errorArg instanceof Error
|
|
117
|
+
? errorArg
|
|
118
|
+
: new Error('MinIO lifecycle failed with a non-Error rejection.');
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
interface IMinioContainerInspection {
|
|
122
|
+
id: string;
|
|
123
|
+
name: string;
|
|
124
|
+
running: boolean;
|
|
125
|
+
portBindings: Record<string, unknown>;
|
|
126
|
+
environmentEntries: string[];
|
|
127
|
+
labels: Record<string, string>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const isUnknownRecord = (valueArg: unknown): valueArg is Record<string, unknown> => {
|
|
131
|
+
return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const parseMinioInspection = (
|
|
135
|
+
stdoutArg: string,
|
|
136
|
+
expectedContainerNameArg: string,
|
|
137
|
+
): IMinioContainerInspection => {
|
|
138
|
+
let parsed: unknown;
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse(stdoutArg);
|
|
141
|
+
} catch {
|
|
142
|
+
throw new Error(`Docker returned an invalid MinIO inspection for ${expectedContainerNameArg}.`);
|
|
143
|
+
}
|
|
144
|
+
if (!Array.isArray(parsed) || parsed.length !== 1 || !isUnknownRecord(parsed[0])) {
|
|
145
|
+
throw new Error(`Docker returned an invalid MinIO inspection for ${expectedContainerNameArg}.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const inspected = parsed[0];
|
|
149
|
+
const state = inspected.State;
|
|
150
|
+
const config = inspected.Config;
|
|
151
|
+
const hostConfig = inspected.HostConfig;
|
|
152
|
+
const normalizedName =
|
|
153
|
+
typeof inspected.Name === 'string' ? inspected.Name.replace(/^\//u, '') : undefined;
|
|
154
|
+
if (
|
|
155
|
+
typeof inspected.Id !== 'string' ||
|
|
156
|
+
!/^[a-f0-9]{64}$/u.test(inspected.Id) ||
|
|
157
|
+
normalizedName !== expectedContainerNameArg ||
|
|
158
|
+
!isUnknownRecord(state) ||
|
|
159
|
+
typeof state.Running !== 'boolean' ||
|
|
160
|
+
!isUnknownRecord(config) ||
|
|
161
|
+
!isUnknownRecord(hostConfig) ||
|
|
162
|
+
!isUnknownRecord(hostConfig.PortBindings) ||
|
|
163
|
+
!Array.isArray(config.Env) ||
|
|
164
|
+
!config.Env.every((entryArg) => typeof entryArg === 'string')
|
|
165
|
+
) {
|
|
166
|
+
throw new Error(`Docker returned an invalid MinIO inspection for ${expectedContainerNameArg}.`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const labels: Record<string, string> = {};
|
|
170
|
+
if (config.Labels !== null && config.Labels !== undefined) {
|
|
171
|
+
if (!isUnknownRecord(config.Labels)) {
|
|
172
|
+
throw new Error(`Docker returned an invalid MinIO inspection for ${expectedContainerNameArg}.`);
|
|
173
|
+
}
|
|
174
|
+
for (const [key, value] of Object.entries(config.Labels)) {
|
|
175
|
+
if (typeof value !== 'string') {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Docker returned an invalid MinIO inspection for ${expectedContainerNameArg}.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
labels[key] = value;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
id: inspected.Id,
|
|
186
|
+
name: normalizedName,
|
|
187
|
+
running: state.Running,
|
|
188
|
+
portBindings: hostConfig.PortBindings,
|
|
189
|
+
environmentEntries: config.Env,
|
|
190
|
+
labels,
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const minioPortMatches = (
|
|
195
|
+
portBindingsArg: Record<string, unknown>,
|
|
196
|
+
containerPortArg: string,
|
|
197
|
+
expectedHostPortArg: string,
|
|
198
|
+
): boolean => {
|
|
199
|
+
const bindings = portBindingsArg[containerPortArg];
|
|
200
|
+
return (
|
|
201
|
+
Array.isArray(bindings) &&
|
|
202
|
+
bindings.some(
|
|
203
|
+
(bindingArg) =>
|
|
204
|
+
isUnknownRecord(bindingArg) && bindingArg.HostPort === expectedHostPortArg,
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** Converge a running MinIO container to an authenticated, verified bucket. */
|
|
210
|
+
export const reconcileMinioBucket = async (
|
|
211
|
+
optionsArg: IMinioReconciliationOptions,
|
|
212
|
+
): Promise<void> => {
|
|
213
|
+
if (!/^[a-f0-9]{64}$/u.test(optionsArg.containerId)) {
|
|
214
|
+
throw new Error('MinIO reconciliation requires a full immutable Docker container id.');
|
|
215
|
+
}
|
|
216
|
+
const clock = optionsArg.clock || defaultMinioClock;
|
|
217
|
+
const deadlineAt = optionsArg.deadlineAt ?? clock.now() + minioReconciliationTimeoutMs;
|
|
218
|
+
if (!Number.isFinite(deadlineAt)) {
|
|
219
|
+
throw new Error('MinIO reconciliation deadline must be finite');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const remainingMs = (): number => Math.floor(deadlineAt - clock.now());
|
|
223
|
+
const execOnce = async (
|
|
224
|
+
stageArg: string,
|
|
225
|
+
argvArg: readonly string[],
|
|
226
|
+
timeoutCapMsArg?: number,
|
|
227
|
+
): Promise<IDockerExecArgvResult> => {
|
|
228
|
+
const remaining = remainingMs();
|
|
229
|
+
if (remaining <= 0) {
|
|
230
|
+
throw minioStageError(stageArg, true);
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
return await optionsArg.docker.execArgv(argvArg, {
|
|
234
|
+
timeoutMs: Math.max(
|
|
235
|
+
1,
|
|
236
|
+
timeoutCapMsArg === undefined ? remaining : Math.min(timeoutCapMsArg, remaining),
|
|
237
|
+
),
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
throw minioStageError(stageArg, remainingMs() <= 0, error);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const execUntilReady = async (
|
|
244
|
+
stageArg: string,
|
|
245
|
+
argvArg: readonly string[],
|
|
246
|
+
): Promise<IDockerExecArgvResult> => {
|
|
247
|
+
let lastCause: unknown;
|
|
248
|
+
while (true) {
|
|
249
|
+
try {
|
|
250
|
+
return await execOnce(stageArg, argvArg, minioProbeTimeoutCapMs);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
const currentCause = minioStageCause(error);
|
|
253
|
+
if (currentCause !== undefined) {
|
|
254
|
+
lastCause = currentCause;
|
|
255
|
+
}
|
|
256
|
+
const remaining = remainingMs();
|
|
257
|
+
if (remaining <= 0) {
|
|
258
|
+
throw minioStageError(stageArg, true, lastCause);
|
|
259
|
+
}
|
|
260
|
+
await clock.delay(Math.min(minioProbeIntervalMs, remaining));
|
|
261
|
+
if (remainingMs() <= 0) {
|
|
262
|
+
throw minioStageError(stageArg, true, lastCause);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
await execUntilReady('container readiness', ['exec', optionsArg.containerId, 'true']);
|
|
269
|
+
// The fixed script reads credentials only from the container environment.
|
|
270
|
+
await execUntilReady('alias configuration', [
|
|
271
|
+
'exec',
|
|
272
|
+
optionsArg.containerId,
|
|
273
|
+
'sh',
|
|
274
|
+
'-c',
|
|
275
|
+
minioAliasSetupScript,
|
|
276
|
+
]);
|
|
277
|
+
await execUntilReady('authenticated readiness', [
|
|
278
|
+
'exec',
|
|
279
|
+
optionsArg.containerId,
|
|
280
|
+
'mc',
|
|
281
|
+
'ready',
|
|
282
|
+
'--json',
|
|
283
|
+
minioAlias,
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
const bucketTarget = `${minioAlias}/${optionsArg.bucket}`;
|
|
287
|
+
await execOnce('bucket creation', [
|
|
288
|
+
'exec',
|
|
289
|
+
optionsArg.containerId,
|
|
290
|
+
'mc',
|
|
291
|
+
'mb',
|
|
292
|
+
'--ignore-existing',
|
|
293
|
+
bucketTarget,
|
|
294
|
+
]);
|
|
295
|
+
await execOnce('bucket verification', [
|
|
296
|
+
'exec',
|
|
297
|
+
optionsArg.containerId,
|
|
298
|
+
'mc',
|
|
299
|
+
'stat',
|
|
300
|
+
bucketTarget,
|
|
301
|
+
]);
|
|
302
|
+
};
|
|
303
|
+
|
|
58
304
|
export class ServiceManager {
|
|
59
305
|
private config: ServiceConfiguration;
|
|
60
306
|
private docker: DockerContainer;
|
|
61
307
|
private enabledServices: string[] | null = null;
|
|
62
308
|
private globalRegistry: GlobalRegistry;
|
|
309
|
+
private minioClock: IMinioReconciliationClock = defaultMinioClock;
|
|
63
310
|
|
|
64
311
|
constructor() {
|
|
65
312
|
this.config = new ServiceConfiguration();
|
|
@@ -598,137 +845,328 @@ export class ServiceManager {
|
|
|
598
845
|
logger.log('error', ' Replica set not ready after 30s — transactions unavailable');
|
|
599
846
|
return false;
|
|
600
847
|
}
|
|
848
|
+
|
|
849
|
+
private async inspectMinioContainer(
|
|
850
|
+
containerReferenceArg: string,
|
|
851
|
+
expectedContainerNameArg: string,
|
|
852
|
+
dockerTimeoutArg: () => { timeoutMs: number },
|
|
853
|
+
): Promise<IMinioContainerInspection> {
|
|
854
|
+
const result = await this.docker.execArgv(
|
|
855
|
+
['inspect', containerReferenceArg],
|
|
856
|
+
dockerTimeoutArg(),
|
|
857
|
+
);
|
|
858
|
+
return parseMinioInspection(result.stdout, expectedContainerNameArg);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
private async assertMinioOwnership(
|
|
862
|
+
inspectionArg: IMinioContainerInspection,
|
|
863
|
+
expectedLabelsArg: Record<string, string>,
|
|
864
|
+
allowLegacyRegistryArg: boolean,
|
|
865
|
+
): Promise<void> {
|
|
866
|
+
const actualGitZoneLabels = Object.entries(inspectionArg.labels).filter(([key]) =>
|
|
867
|
+
key.startsWith('git.zone.'),
|
|
868
|
+
);
|
|
869
|
+
const expectedEntries = Object.entries(expectedLabelsArg);
|
|
870
|
+
const hasExactCurrentLabels =
|
|
871
|
+
actualGitZoneLabels.length === expectedEntries.length &&
|
|
872
|
+
expectedEntries.every(([key, value]) => inspectionArg.labels[key] === value);
|
|
873
|
+
if (hasExactCurrentLabels) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const refusal = (): Error =>
|
|
878
|
+
new Error(
|
|
879
|
+
`Refusing to use MinIO container ${inspectionArg.name}: ` +
|
|
880
|
+
'its ownership is not proven for the current project.',
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
if (!allowLegacyRegistryArg) {
|
|
884
|
+
throw refusal();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Any ownership label is authoritative. A partial, stale, or foreign set
|
|
888
|
+
// must never be reinterpreted through the legacy registry fallback.
|
|
889
|
+
if (actualGitZoneLabels.length > 0 || Object.keys(inspectionArg.labels).length > 0) {
|
|
890
|
+
throw refusal();
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Containers created before labels were introduced may be reclaimed only
|
|
894
|
+
// when the old global registry has one unambiguous claim for this exact
|
|
895
|
+
// name and the current resolved project path.
|
|
896
|
+
const currentProjectPath = plugins.path.resolve(process.cwd());
|
|
897
|
+
const registryProjects = await this.globalRegistry.getAllProjects();
|
|
898
|
+
const claims = Object.entries(registryProjects).filter(
|
|
899
|
+
([, project]) => project.containers.minio === inspectionArg.name,
|
|
900
|
+
);
|
|
901
|
+
if (claims.length !== 1) {
|
|
902
|
+
throw refusal();
|
|
903
|
+
}
|
|
904
|
+
const [registryPath, project] = claims[0];
|
|
905
|
+
if (
|
|
906
|
+
plugins.path.resolve(registryPath) !== currentProjectPath ||
|
|
907
|
+
plugins.path.resolve(project.projectPath) !== currentProjectPath
|
|
908
|
+
) {
|
|
909
|
+
throw refusal();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private async revalidateMinioContainer(
|
|
914
|
+
containerIdArg: string,
|
|
915
|
+
expectedContainerNameArg: string,
|
|
916
|
+
expectedLabelsArg: Record<string, string>,
|
|
917
|
+
allowLegacyRegistryArg: boolean,
|
|
918
|
+
dockerTimeoutArg: () => { timeoutMs: number },
|
|
919
|
+
): Promise<IMinioContainerInspection> {
|
|
920
|
+
const inspection = await this.inspectMinioContainer(
|
|
921
|
+
containerIdArg,
|
|
922
|
+
expectedContainerNameArg,
|
|
923
|
+
dockerTimeoutArg,
|
|
924
|
+
);
|
|
925
|
+
if (inspection.id !== containerIdArg) {
|
|
926
|
+
throw new Error(
|
|
927
|
+
`Refusing to use MinIO container ${expectedContainerNameArg}: ` +
|
|
928
|
+
'its immutable id changed during reconciliation.',
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
await this.assertMinioOwnership(
|
|
932
|
+
inspection,
|
|
933
|
+
expectedLabelsArg,
|
|
934
|
+
allowLegacyRegistryArg,
|
|
935
|
+
);
|
|
936
|
+
return inspection;
|
|
937
|
+
}
|
|
601
938
|
|
|
602
939
|
/**
|
|
603
940
|
* Start MinIO service
|
|
604
941
|
*/
|
|
605
942
|
public async startMinIO(): Promise<void> {
|
|
606
943
|
logger.log('note', '📦 S3/MinIO:');
|
|
607
|
-
|
|
944
|
+
|
|
608
945
|
const config = this.config.getConfig();
|
|
609
946
|
const containers = this.config.getContainerNames();
|
|
610
947
|
const directories = this.config.getDataDirectories();
|
|
611
|
-
|
|
948
|
+
|
|
949
|
+
// Validation lives on the mutating path so recovery commands remain usable.
|
|
950
|
+
const bucketValidationError = getS3BucketNameValidationError(config.S3_BUCKET);
|
|
951
|
+
if (bucketValidationError) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
`Invalid S3_BUCKET in .nogit/env.json: ${bucketValidationError}. ` +
|
|
954
|
+
'Choose a valid custom bucket name or remove the field to regenerate the project default.',
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
612
958
|
// Ensure data directory exists and is marked as tool-owned
|
|
613
959
|
const minioLabels = await this.prepareDataDirectory('minio', directories.minio);
|
|
960
|
+
const deadlineAt = this.minioClock.now() + minioReconciliationTimeoutMs;
|
|
961
|
+
const dockerTimeout = (): { timeoutMs: number } => {
|
|
962
|
+
const remaining = Math.floor(deadlineAt - this.minioClock.now());
|
|
963
|
+
if (remaining <= 0) {
|
|
964
|
+
throw new Error(
|
|
965
|
+
'MinIO container setup did not complete before the shared reconciliation deadline.',
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
return { timeoutMs: Math.max(1, remaining) };
|
|
969
|
+
};
|
|
614
970
|
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
// Check if port mapping matches config
|
|
624
|
-
const minioPortMappings = await this.docker.getPortMappings(containers.minio);
|
|
625
|
-
if (minioPortMappings &&
|
|
626
|
-
(minioPortMappings['9000'] !== config.S3_PORT ||
|
|
627
|
-
minioPortMappings['9001'] !== config.S3_CONSOLE_PORT)) {
|
|
628
|
-
logger.log('note', ' Port configuration changed, recreating container...');
|
|
629
|
-
await this.docker.remove(containers.minio, true);
|
|
630
|
-
// Fall through to create new container
|
|
631
|
-
const success = await this.docker.run({
|
|
632
|
-
name: containers.minio,
|
|
633
|
-
image: 'minio/minio',
|
|
634
|
-
ports: {
|
|
635
|
-
[config.S3_PORT]: '9000',
|
|
636
|
-
[config.S3_CONSOLE_PORT]: '9001'
|
|
637
|
-
},
|
|
638
|
-
volumes: {
|
|
639
|
-
[directories.minio]: '/data'
|
|
640
|
-
},
|
|
641
|
-
environment: {
|
|
642
|
-
MINIO_ROOT_USER: config.S3_ACCESSKEY,
|
|
643
|
-
MINIO_ROOT_PASSWORD: config.S3_SECRETKEY
|
|
644
|
-
},
|
|
645
|
-
labels: minioLabels,
|
|
646
|
-
restart: 'unless-stopped',
|
|
647
|
-
command: 'server /data --console-address ":9001"'
|
|
648
|
-
});
|
|
971
|
+
const startedContainerIds: string[] = [];
|
|
972
|
+
const startById = async (containerIdArg: string): Promise<void> => {
|
|
973
|
+
const executionOptions = dockerTimeout();
|
|
974
|
+
// Record ownership of the lifecycle before awaiting Docker: a timed-out
|
|
975
|
+
// start may still have reached the daemon and must be rolled back.
|
|
976
|
+
startedContainerIds.push(containerIdArg);
|
|
977
|
+
await this.docker.execArgv(['start', containerIdArg], executionOptions);
|
|
978
|
+
};
|
|
649
979
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
case 'not_exists':
|
|
682
|
-
logger.log('note', ' Creating container...');
|
|
683
|
-
|
|
684
|
-
const success = await this.docker.run({
|
|
685
|
-
name: containers.minio,
|
|
686
|
-
image: 'minio/minio',
|
|
687
|
-
ports: {
|
|
688
|
-
[config.S3_PORT]: '9000',
|
|
689
|
-
[config.S3_CONSOLE_PORT]: '9001'
|
|
690
|
-
},
|
|
691
|
-
volumes: {
|
|
692
|
-
[directories.minio]: '/data'
|
|
693
|
-
},
|
|
694
|
-
environment: {
|
|
695
|
-
MINIO_ROOT_USER: config.S3_ACCESSKEY,
|
|
696
|
-
MINIO_ROOT_PASSWORD: config.S3_SECRETKEY
|
|
697
|
-
},
|
|
698
|
-
labels: minioLabels,
|
|
699
|
-
restart: 'unless-stopped',
|
|
700
|
-
command: 'server /data --console-address ":9001"'
|
|
701
|
-
});
|
|
980
|
+
let outcome: string;
|
|
981
|
+
try {
|
|
982
|
+
const listResult = await this.docker.execArgv(
|
|
983
|
+
[
|
|
984
|
+
'container',
|
|
985
|
+
'ls',
|
|
986
|
+
'-a',
|
|
987
|
+
'--filter',
|
|
988
|
+
`name=${containers.minio}`,
|
|
989
|
+
'--format',
|
|
990
|
+
'{{.Names}}',
|
|
991
|
+
],
|
|
992
|
+
dockerTimeout(),
|
|
993
|
+
);
|
|
994
|
+
const containerExists = listResult.stdout
|
|
995
|
+
.split(/\r?\n/u)
|
|
996
|
+
.map((lineArg) => lineArg.trim())
|
|
997
|
+
.includes(containers.minio);
|
|
998
|
+
|
|
999
|
+
let initialInspection: IMinioContainerInspection | undefined;
|
|
1000
|
+
if (containerExists) {
|
|
1001
|
+
initialInspection = await this.inspectMinioContainer(
|
|
1002
|
+
containers.minio,
|
|
1003
|
+
containers.minio,
|
|
1004
|
+
dockerTimeout,
|
|
1005
|
+
);
|
|
1006
|
+
// No existing container is started or receives an in-container command
|
|
1007
|
+
// until it proves current or unambiguous legacy ownership.
|
|
1008
|
+
await this.assertMinioOwnership(initialInspection, minioLabels, true);
|
|
1009
|
+
}
|
|
702
1010
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1011
|
+
let needsRecreate = false;
|
|
1012
|
+
let credentialsChanged = false;
|
|
1013
|
+
if (initialInspection) {
|
|
1014
|
+
const apiMatches = minioPortMatches(
|
|
1015
|
+
initialInspection.portBindings,
|
|
1016
|
+
'9000/tcp',
|
|
1017
|
+
config.S3_PORT,
|
|
1018
|
+
);
|
|
1019
|
+
const consoleMatches = minioPortMatches(
|
|
1020
|
+
initialInspection.portBindings,
|
|
1021
|
+
'9001/tcp',
|
|
1022
|
+
config.S3_CONSOLE_PORT,
|
|
1023
|
+
);
|
|
1024
|
+
const readExactEnvironmentValue = (nameArg: string): string | undefined => {
|
|
1025
|
+
const prefix = `${nameArg}=`;
|
|
1026
|
+
const matches = initialInspection.environmentEntries.filter((entryArg) =>
|
|
1027
|
+
entryArg.startsWith(prefix),
|
|
718
1028
|
);
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1029
|
+
return matches.length === 1 ? matches[0].slice(prefix.length) : undefined;
|
|
1030
|
+
};
|
|
1031
|
+
credentialsChanged =
|
|
1032
|
+
readExactEnvironmentValue('MINIO_ROOT_USER') !== config.S3_ACCESSKEY ||
|
|
1033
|
+
readExactEnvironmentValue('MINIO_ROOT_PASSWORD') !== config.S3_SECRETKEY;
|
|
1034
|
+
needsRecreate = !apiMatches || !consoleMatches || credentialsChanged;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
let activeInspection: IMinioContainerInspection;
|
|
1038
|
+
let activeAllowsLegacyRegistry: boolean;
|
|
1039
|
+
if (initialInspection && needsRecreate) {
|
|
1040
|
+
logger.log(
|
|
1041
|
+
'note',
|
|
1042
|
+
credentialsChanged
|
|
1043
|
+
? ' Credential configuration changed, recreating container...'
|
|
1044
|
+
: ' Port configuration changed, recreating container...',
|
|
1045
|
+
);
|
|
1046
|
+
await this.revalidateMinioContainer(
|
|
1047
|
+
initialInspection.id,
|
|
1048
|
+
containers.minio,
|
|
1049
|
+
minioLabels,
|
|
1050
|
+
true,
|
|
1051
|
+
dockerTimeout,
|
|
1052
|
+
);
|
|
1053
|
+
await this.docker.execArgv(['rm', '-f', initialInspection.id], dockerTimeout());
|
|
1054
|
+
activeInspection = await this.createMinioContainer(minioLabels, dockerTimeout);
|
|
1055
|
+
activeAllowsLegacyRegistry = false;
|
|
1056
|
+
await startById(activeInspection.id);
|
|
1057
|
+
outcome = 'Recreated with configured ports and reconciled';
|
|
1058
|
+
} else if (initialInspection && !initialInspection.running) {
|
|
1059
|
+
logger.log('note', ' Starting existing container...');
|
|
1060
|
+
activeInspection = initialInspection;
|
|
1061
|
+
activeAllowsLegacyRegistry = true;
|
|
1062
|
+
await startById(activeInspection.id);
|
|
1063
|
+
outcome = 'Started and reconciled';
|
|
1064
|
+
} else if (!initialInspection) {
|
|
1065
|
+
logger.log('note', ' Creating container...');
|
|
1066
|
+
activeInspection = await this.createMinioContainer(minioLabels, dockerTimeout);
|
|
1067
|
+
activeAllowsLegacyRegistry = false;
|
|
1068
|
+
await startById(activeInspection.id);
|
|
1069
|
+
outcome = 'Created, started, and reconciled';
|
|
1070
|
+
} else {
|
|
1071
|
+
logger.log('note', ' Already running; reconciling bucket...');
|
|
1072
|
+
activeInspection = initialInspection;
|
|
1073
|
+
activeAllowsLegacyRegistry = true;
|
|
1074
|
+
outcome = 'Already running and reconciled';
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const reconciliationInspection = await this.revalidateMinioContainer(
|
|
1078
|
+
activeInspection.id,
|
|
1079
|
+
containers.minio,
|
|
1080
|
+
minioLabels,
|
|
1081
|
+
activeAllowsLegacyRegistry,
|
|
1082
|
+
dockerTimeout,
|
|
1083
|
+
);
|
|
1084
|
+
await reconcileMinioBucket({
|
|
1085
|
+
docker: this.docker,
|
|
1086
|
+
containerId: reconciliationInspection.id,
|
|
1087
|
+
bucket: config.S3_BUCKET,
|
|
1088
|
+
deadlineAt,
|
|
1089
|
+
clock: this.minioClock,
|
|
1090
|
+
});
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
const originalError = normalizeMinioLifecycleError(error);
|
|
1093
|
+
const rollbackErrors: Error[] = [];
|
|
1094
|
+
for (let index = startedContainerIds.length - 1; index >= 0; index--) {
|
|
1095
|
+
try {
|
|
1096
|
+
await this.docker.execArgv(['stop', startedContainerIds[index]], {
|
|
1097
|
+
timeoutMs: minioRollbackTimeoutMs,
|
|
1098
|
+
});
|
|
1099
|
+
} catch (rollbackError) {
|
|
1100
|
+
rollbackErrors.push(normalizeMinioLifecycleError(rollbackError));
|
|
723
1101
|
}
|
|
724
|
-
|
|
1102
|
+
}
|
|
1103
|
+
if (rollbackErrors.length > 0) {
|
|
1104
|
+
throw new AggregateError(
|
|
1105
|
+
[originalError, ...rollbackErrors],
|
|
1106
|
+
'MinIO startup failed and rollback could not stop every container started by this invocation.',
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
throw originalError;
|
|
725
1110
|
}
|
|
726
|
-
|
|
1111
|
+
|
|
1112
|
+
logger.log('ok', ` ${outcome} ✓`);
|
|
1113
|
+
logger.log('ok', ` Bucket '${config.S3_BUCKET}' exists and is authenticated ✓`);
|
|
727
1114
|
logger.log('info', ` Container: ${containers.minio}`);
|
|
728
1115
|
logger.log('info', ` Port: ${config.S3_PORT}`);
|
|
729
1116
|
logger.log('info', ` Bucket: ${config.S3_BUCKET}`);
|
|
730
1117
|
logger.log('info', ` API: http://${config.S3_HOST}:${config.S3_PORT}`);
|
|
731
|
-
logger.log(
|
|
1118
|
+
logger.log(
|
|
1119
|
+
'info',
|
|
1120
|
+
` Console: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT} (credentials: ***)`,
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/** Create stopped MinIO, inspect its id, and prove exact current labels. */
|
|
1125
|
+
private async createMinioContainer(
|
|
1126
|
+
labelsArg: { [key: string]: string },
|
|
1127
|
+
dockerTimeoutArg: () => { timeoutMs: number },
|
|
1128
|
+
): Promise<IMinioContainerInspection> {
|
|
1129
|
+
const config = this.config.getConfig();
|
|
1130
|
+
const containers = this.config.getContainerNames();
|
|
1131
|
+
const directories = this.config.getDataDirectories();
|
|
1132
|
+
const creationResult = await this.docker.createArgv(
|
|
1133
|
+
{
|
|
1134
|
+
name: containers.minio,
|
|
1135
|
+
image: 'minio/minio',
|
|
1136
|
+
ports: {
|
|
1137
|
+
[config.S3_PORT]: '9000',
|
|
1138
|
+
[config.S3_CONSOLE_PORT]: '9001',
|
|
1139
|
+
},
|
|
1140
|
+
volumes: {
|
|
1141
|
+
[directories.minio]: '/data',
|
|
1142
|
+
},
|
|
1143
|
+
environment: {
|
|
1144
|
+
MINIO_ROOT_USER: config.S3_ACCESSKEY,
|
|
1145
|
+
MINIO_ROOT_PASSWORD: config.S3_SECRETKEY,
|
|
1146
|
+
},
|
|
1147
|
+
labels: labelsArg,
|
|
1148
|
+
restart: 'unless-stopped',
|
|
1149
|
+
command: ['server', '/data', '--console-address', ':9001'],
|
|
1150
|
+
},
|
|
1151
|
+
dockerTimeoutArg(),
|
|
1152
|
+
);
|
|
1153
|
+
const containerId = creationResult.stdout.trim();
|
|
1154
|
+
if (!/^[a-f0-9]{64}$/u.test(containerId)) {
|
|
1155
|
+
throw new Error(`Docker returned an invalid id while creating ${containers.minio}.`);
|
|
1156
|
+
}
|
|
1157
|
+
const inspection = await this.inspectMinioContainer(
|
|
1158
|
+
containerId,
|
|
1159
|
+
containers.minio,
|
|
1160
|
+
dockerTimeoutArg,
|
|
1161
|
+
);
|
|
1162
|
+
if (inspection.id !== containerId) {
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
`Refusing to start MinIO container ${containers.minio}: ` +
|
|
1165
|
+
'Docker inspection did not return the created immutable id.',
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
await this.assertMinioOwnership(inspection, labelsArg, false);
|
|
1169
|
+
return inspection;
|
|
732
1170
|
}
|
|
733
1171
|
|
|
734
1172
|
/**
|
|
@@ -1144,7 +1582,7 @@ export class ServiceManager {
|
|
|
1144
1582
|
logger.log('info', ` Host: ${config.S3_HOST}`);
|
|
1145
1583
|
logger.log('info', ` API Port: ${config.S3_PORT}`);
|
|
1146
1584
|
logger.log('info', ` Console Port: ${config.S3_CONSOLE_PORT}`);
|
|
1147
|
-
logger.log('info',
|
|
1585
|
+
logger.log('info', ' Access Key: ***');
|
|
1148
1586
|
logger.log('info', ' Secret Key: ***');
|
|
1149
1587
|
logger.log('info', ` Bucket: ${config.S3_BUCKET}`);
|
|
1150
1588
|
logger.log('info', ` Use SSL: ${config.S3_USESSL}`);
|