@git.zone/cli 2.22.0 → 2.24.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mod_services/classes.dockercontainer.d.ts +92 -0
- package/dist_ts/mod_services/classes.dockercontainer.js +225 -6
- package/dist_ts/mod_services/classes.globalregistry.d.ts +10 -0
- package/dist_ts/mod_services/classes.globalregistry.js +23 -1
- package/dist_ts/mod_services/classes.serviceconfiguration.d.ts +39 -1
- package/dist_ts/mod_services/classes.serviceconfiguration.js +89 -22
- package/dist_ts/mod_services/classes.servicedatamarker.d.ts +93 -0
- package/dist_ts/mod_services/classes.servicedatamarker.js +166 -0
- package/dist_ts/mod_services/classes.servicemanager.d.ts +103 -3
- package/dist_ts/mod_services/classes.servicemanager.js +352 -100
- package/dist_ts/mod_services/classes.servicepruner.d.ts +102 -0
- package/dist_ts/mod_services/classes.servicepruner.js +410 -0
- package/dist_ts/mod_services/helpers.d.ts +15 -0
- package/dist_ts/mod_services/helpers.js +57 -1
- package/dist_ts/mod_services/index.js +307 -53
- package/package.json +4 -3
- package/readme.hints.md +88 -0
- package/readme.md +71 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mod_services/classes.dockercontainer.ts +269 -9
- package/ts/mod_services/classes.globalregistry.ts +27 -0
- package/ts/mod_services/classes.serviceconfiguration.ts +105 -24
- package/ts/mod_services/classes.servicedatamarker.ts +228 -0
- package/ts/mod_services/classes.servicemanager.ts +480 -117
- package/ts/mod_services/classes.servicepruner.ts +532 -0
- package/ts/mod_services/helpers.ts +60 -0
- package/ts/mod_services/index.ts +437 -58
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type TServiceName } from './classes.servicedatamarker.js';
|
|
2
|
+
/** Default inactivity threshold before a project is even considered stale. */
|
|
3
|
+
export declare const defaultStaleDays = 30;
|
|
4
|
+
/**
|
|
5
|
+
* How a project was classified.
|
|
6
|
+
*
|
|
7
|
+
* - `live` — recent activity; never a reclamation candidate.
|
|
8
|
+
* - `stale` — project still on disk but inactive past the threshold.
|
|
9
|
+
* - `orphaned` — project directory no longer exists.
|
|
10
|
+
* - `unknown` — insufficient evidence to classify; never reclaimed.
|
|
11
|
+
*/
|
|
12
|
+
export type TServiceProjectState = 'live' | 'stale' | 'orphaned' | 'unknown';
|
|
13
|
+
/** Why a candidate was left alone. */
|
|
14
|
+
export interface IServiceSkipReason {
|
|
15
|
+
target: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
}
|
|
18
|
+
export interface IServiceDataDirReport {
|
|
19
|
+
service: TServiceName;
|
|
20
|
+
path: string;
|
|
21
|
+
exists: boolean;
|
|
22
|
+
sizeBytes: number;
|
|
23
|
+
reclaimable: boolean;
|
|
24
|
+
reason: string;
|
|
25
|
+
}
|
|
26
|
+
export interface IServiceContainerReport {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
state: string;
|
|
30
|
+
running: boolean;
|
|
31
|
+
reclaimable: boolean;
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
|
+
export interface IServiceProjectReport {
|
|
35
|
+
projectPath: string;
|
|
36
|
+
projectName: string;
|
|
37
|
+
state: TServiceProjectState;
|
|
38
|
+
stateReason: string;
|
|
39
|
+
lastActive: number | null;
|
|
40
|
+
inactiveDays: number | null;
|
|
41
|
+
projectDirExists: boolean;
|
|
42
|
+
registered: boolean;
|
|
43
|
+
markerPresent: boolean;
|
|
44
|
+
containers: IServiceContainerReport[];
|
|
45
|
+
dataDirectories: IServiceDataDirReport[];
|
|
46
|
+
totalDataBytes: number;
|
|
47
|
+
reclaimableDataBytes: number;
|
|
48
|
+
}
|
|
49
|
+
export interface IServicePrunePlan {
|
|
50
|
+
staleDays: number;
|
|
51
|
+
dockerAvailable: boolean;
|
|
52
|
+
projects: IServiceProjectReport[];
|
|
53
|
+
registryEntriesToRemove: string[];
|
|
54
|
+
containersToRemove: Array<{
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
projectPath: string;
|
|
58
|
+
}>;
|
|
59
|
+
dataDirsToRemove: Array<{
|
|
60
|
+
projectPath: string;
|
|
61
|
+
service: TServiceName;
|
|
62
|
+
path: string;
|
|
63
|
+
sizeBytes: number;
|
|
64
|
+
}>;
|
|
65
|
+
skipped: IServiceSkipReason[];
|
|
66
|
+
totals: {
|
|
67
|
+
projects: number;
|
|
68
|
+
totalDataBytes: number;
|
|
69
|
+
reclaimableDataBytes: number;
|
|
70
|
+
/** Bytes that would be reclaimable if the holding containers were stopped first. */
|
|
71
|
+
blockedByRunningBytes: number;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface IServicePrunerOptions {
|
|
75
|
+
staleDays?: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Plans and applies reclamation of resources created by `gitzone services`.
|
|
79
|
+
*
|
|
80
|
+
* Design mirrors `@git.zone/tsdocker`'s `TsDockerPruner`: build an explicit
|
|
81
|
+
* plan, print it, and only mutate on an explicit apply that re-verifies every
|
|
82
|
+
* single item. Nothing is ever matched by image or by bare name pattern, and
|
|
83
|
+
* every ambiguity resolves to "skip".
|
|
84
|
+
*/
|
|
85
|
+
export declare class ServicePruner {
|
|
86
|
+
private docker;
|
|
87
|
+
private globalRegistry;
|
|
88
|
+
private staleDays;
|
|
89
|
+
constructor(optionsArg?: IServicePrunerOptions);
|
|
90
|
+
/**
|
|
91
|
+
* Build a reclamation plan. Read-only: touches nothing.
|
|
92
|
+
*/
|
|
93
|
+
createPlan(): Promise<IServicePrunePlan>;
|
|
94
|
+
/**
|
|
95
|
+
* Apply a plan, re-verifying every item immediately before it is removed.
|
|
96
|
+
*
|
|
97
|
+
* Any drift between plan and current reality aborts with an error rather than
|
|
98
|
+
* removing something that may since have become live.
|
|
99
|
+
*/
|
|
100
|
+
applyPlan(planArg: IServicePrunePlan): Promise<void>;
|
|
101
|
+
private isClaimedByExactlyOneProject;
|
|
102
|
+
}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import * as plugins from './mod.plugins.js';
|
|
2
|
+
import * as helpers from './helpers.js';
|
|
3
|
+
import { DockerContainer } from './classes.dockercontainer.js';
|
|
4
|
+
import { GlobalRegistry } from './classes.globalregistry.js';
|
|
5
|
+
import { getServiceDataDirectory, isSafeServiceDataPath, isServiceOwnedByLabels, readServiceDataMarker, serviceImages, serviceNames, serviceToolLabel, } from './classes.servicedatamarker.js';
|
|
6
|
+
import { logger } from '../gitzone.logging.js';
|
|
7
|
+
/** Default inactivity threshold before a project is even considered stale. */
|
|
8
|
+
export const defaultStaleDays = 30;
|
|
9
|
+
const dayInMs = 24 * 60 * 60 * 1000;
|
|
10
|
+
/**
|
|
11
|
+
* Plans and applies reclamation of resources created by `gitzone services`.
|
|
12
|
+
*
|
|
13
|
+
* Design mirrors `@git.zone/tsdocker`'s `TsDockerPruner`: build an explicit
|
|
14
|
+
* plan, print it, and only mutate on an explicit apply that re-verifies every
|
|
15
|
+
* single item. Nothing is ever matched by image or by bare name pattern, and
|
|
16
|
+
* every ambiguity resolves to "skip".
|
|
17
|
+
*/
|
|
18
|
+
export class ServicePruner {
|
|
19
|
+
docker;
|
|
20
|
+
globalRegistry;
|
|
21
|
+
staleDays;
|
|
22
|
+
constructor(optionsArg = {}) {
|
|
23
|
+
this.docker = new DockerContainer();
|
|
24
|
+
this.globalRegistry = GlobalRegistry.getInstance();
|
|
25
|
+
this.staleDays = optionsArg.staleDays ?? defaultStaleDays;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build a reclamation plan. Read-only: touches nothing.
|
|
29
|
+
*/
|
|
30
|
+
async createPlan() {
|
|
31
|
+
const skipped = [];
|
|
32
|
+
const registryProjects = await this.globalRegistry.getAllProjects();
|
|
33
|
+
// Collect tool-owned containers. A Docker failure must not be mistaken for
|
|
34
|
+
// "no containers exist", so we record availability and fail closed below.
|
|
35
|
+
let dockerAvailable = true;
|
|
36
|
+
let labeledContainers = [];
|
|
37
|
+
let runningMountSources = [];
|
|
38
|
+
try {
|
|
39
|
+
const ids = await this.docker.listIds([`label=git.zone.tool=${serviceToolLabel}`]);
|
|
40
|
+
labeledContainers = await this.docker.inspectMany(ids);
|
|
41
|
+
runningMountSources = await this.docker.listRunningMountSources();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
dockerAvailable = false;
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
skipped.push({
|
|
47
|
+
target: 'docker',
|
|
48
|
+
reason: `docker unavailable (${message}); container and data reclamation disabled`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Containers recorded in the registry but not labeled (created by older
|
|
52
|
+
// versions). Their names are only trusted when exactly one registry entry
|
|
53
|
+
// claims them.
|
|
54
|
+
const nameClaims = new Map();
|
|
55
|
+
for (const [projectPath, project] of Object.entries(registryProjects)) {
|
|
56
|
+
for (const containerName of Object.values(project.containers)) {
|
|
57
|
+
if (!containerName) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const claims = nameClaims.get(containerName) || [];
|
|
61
|
+
claims.push(projectPath);
|
|
62
|
+
nameClaims.set(containerName, claims);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let legacyContainers = [];
|
|
66
|
+
if (dockerAvailable && nameClaims.size > 0) {
|
|
67
|
+
try {
|
|
68
|
+
// One `docker ps -a` for all claimed names rather than one per name:
|
|
69
|
+
// each shell-out is a spawned child process, and this runs on every
|
|
70
|
+
// prune. Identity is established by the exact-name match below, so the
|
|
71
|
+
// listing only needs to be a superset.
|
|
72
|
+
const labeledIds = new Set(labeledContainers.map((container) => container.id));
|
|
73
|
+
const allIds = await this.docker.listIds([]);
|
|
74
|
+
const candidateIds = allIds.filter((id) => !labeledIds.has(id));
|
|
75
|
+
const inspected = await this.docker.inspectMany(candidateIds);
|
|
76
|
+
legacyContainers = inspected.filter((container) => nameClaims.has(container.name));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
dockerAvailable = false;
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
skipped.push({
|
|
82
|
+
target: 'docker',
|
|
83
|
+
reason: `docker unavailable (${message}); container and data reclamation disabled`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Group every known container and registry entry under a project path.
|
|
88
|
+
const candidates = new Map();
|
|
89
|
+
const ensureCandidate = (projectPathArg, projectNameArg) => {
|
|
90
|
+
const projectPath = plugins.path.resolve(projectPathArg);
|
|
91
|
+
const existing = candidates.get(projectPath);
|
|
92
|
+
if (existing) {
|
|
93
|
+
return existing;
|
|
94
|
+
}
|
|
95
|
+
const candidate = {
|
|
96
|
+
projectPath,
|
|
97
|
+
projectName: projectNameArg,
|
|
98
|
+
containers: [],
|
|
99
|
+
};
|
|
100
|
+
candidates.set(projectPath, candidate);
|
|
101
|
+
return candidate;
|
|
102
|
+
};
|
|
103
|
+
for (const [projectPath, project] of Object.entries(registryProjects)) {
|
|
104
|
+
const candidate = ensureCandidate(projectPath, project.projectName);
|
|
105
|
+
candidate.registryEntry = project;
|
|
106
|
+
}
|
|
107
|
+
for (const container of labeledContainers) {
|
|
108
|
+
const projectPath = container.labels['git.zone.project-path'];
|
|
109
|
+
if (!projectPath) {
|
|
110
|
+
skipped.push({
|
|
111
|
+
target: container.name,
|
|
112
|
+
reason: 'labeled as tool-owned but carries no git.zone.project-path',
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const candidate = ensureCandidate(projectPath, plugins.path.basename(projectPath));
|
|
117
|
+
candidate.containers.push(container);
|
|
118
|
+
}
|
|
119
|
+
// Projects whose container ownership cannot be resolved. Ambiguity must
|
|
120
|
+
// *tighten* the decision, never loosen it: if we cannot attribute a
|
|
121
|
+
// container, we also cannot claim that a project has no container running,
|
|
122
|
+
// so such projects are forced to `unknown` and nothing is reclaimed.
|
|
123
|
+
const ambiguousProjectPaths = new Set();
|
|
124
|
+
for (const container of legacyContainers) {
|
|
125
|
+
const claims = nameClaims.get(container.name) || [];
|
|
126
|
+
if (claims.length !== 1) {
|
|
127
|
+
skipped.push({
|
|
128
|
+
target: container.name,
|
|
129
|
+
reason: `unlabeled container claimed by ${claims.length} registered projects; ownership ambiguous`,
|
|
130
|
+
});
|
|
131
|
+
for (const claim of claims) {
|
|
132
|
+
ambiguousProjectPaths.add(plugins.path.resolve(claim));
|
|
133
|
+
ensureCandidate(claim, plugins.path.basename(claim));
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const projectPath = claims[0];
|
|
138
|
+
const candidate = ensureCandidate(projectPath, plugins.path.basename(projectPath));
|
|
139
|
+
candidate.containers.push(container);
|
|
140
|
+
}
|
|
141
|
+
// Classify and decide.
|
|
142
|
+
const projects = [];
|
|
143
|
+
const registryEntriesToRemove = [];
|
|
144
|
+
const containersToRemove = [];
|
|
145
|
+
const dataDirsToRemove = [];
|
|
146
|
+
let blockedByRunningBytes = 0;
|
|
147
|
+
for (const candidate of [...candidates.values()].sort((first, second) => first.projectPath.localeCompare(second.projectPath))) {
|
|
148
|
+
const projectDirExists = await plugins.smartfs.directory(candidate.projectPath).exists();
|
|
149
|
+
const marker = await readServiceDataMarker(candidate.projectPath);
|
|
150
|
+
const lastActive = candidate.registryEntry?.lastActive ?? null;
|
|
151
|
+
const inactiveDays = lastActive === null ? null : Math.floor((Date.now() - lastActive) / dayInMs);
|
|
152
|
+
let state;
|
|
153
|
+
let stateReason;
|
|
154
|
+
if (ambiguousProjectPaths.has(candidate.projectPath)) {
|
|
155
|
+
state = 'unknown';
|
|
156
|
+
stateReason =
|
|
157
|
+
'a container name for this project is claimed by more than one project; ownership ambiguous';
|
|
158
|
+
}
|
|
159
|
+
else if (!projectDirExists) {
|
|
160
|
+
state = 'orphaned';
|
|
161
|
+
stateReason = 'project directory no longer exists';
|
|
162
|
+
}
|
|
163
|
+
else if (lastActive === null) {
|
|
164
|
+
state = 'unknown';
|
|
165
|
+
stateReason = 'no lastActive timestamp recorded; cannot prove inactivity';
|
|
166
|
+
}
|
|
167
|
+
else if (inactiveDays > this.staleDays) {
|
|
168
|
+
state = 'stale';
|
|
169
|
+
stateReason = `inactive for ${inactiveDays} days (threshold ${this.staleDays})`;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
state = 'live';
|
|
173
|
+
stateReason = `active ${inactiveDays} day(s) ago`;
|
|
174
|
+
}
|
|
175
|
+
const anyContainerRunning = candidate.containers.some((container) => container.running);
|
|
176
|
+
// Containers
|
|
177
|
+
const containerReports = [];
|
|
178
|
+
for (const container of candidate.containers) {
|
|
179
|
+
let reclaimable = false;
|
|
180
|
+
let reason;
|
|
181
|
+
if (!dockerAvailable) {
|
|
182
|
+
reason = 'docker unavailable';
|
|
183
|
+
}
|
|
184
|
+
else if (state === 'live') {
|
|
185
|
+
reason = 'project is live';
|
|
186
|
+
}
|
|
187
|
+
else if (state === 'unknown') {
|
|
188
|
+
reason = 'project state unknown';
|
|
189
|
+
}
|
|
190
|
+
else if (container.running) {
|
|
191
|
+
reason = 'container is running; stop it first';
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
reclaimable = true;
|
|
195
|
+
reason = `project ${state}, container stopped`;
|
|
196
|
+
}
|
|
197
|
+
containerReports.push({
|
|
198
|
+
id: container.id,
|
|
199
|
+
name: container.name,
|
|
200
|
+
state: container.state,
|
|
201
|
+
running: container.running,
|
|
202
|
+
reclaimable,
|
|
203
|
+
reason,
|
|
204
|
+
});
|
|
205
|
+
if (reclaimable) {
|
|
206
|
+
containersToRemove.push({
|
|
207
|
+
id: container.id,
|
|
208
|
+
name: container.name,
|
|
209
|
+
projectPath: candidate.projectPath,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Data directories
|
|
214
|
+
const ownershipProven = !!marker || !!candidate.registryEntry;
|
|
215
|
+
const dataReports = [];
|
|
216
|
+
let totalDataBytes = 0;
|
|
217
|
+
let reclaimableDataBytes = 0;
|
|
218
|
+
for (const service of serviceNames) {
|
|
219
|
+
const dataPath = getServiceDataDirectory(candidate.projectPath, service);
|
|
220
|
+
const exists = await plugins.smartfs.directory(dataPath).exists();
|
|
221
|
+
const sizeBytes = exists ? await helpers.getDirectorySize(dataPath) : 0;
|
|
222
|
+
totalDataBytes += sizeBytes;
|
|
223
|
+
let reclaimable = false;
|
|
224
|
+
let reason;
|
|
225
|
+
const mountConflict = runningMountSources.find((source) => helpers.pathsOverlap(dataPath, source));
|
|
226
|
+
if (!exists) {
|
|
227
|
+
reason = 'no data directory';
|
|
228
|
+
}
|
|
229
|
+
else if (!dockerAvailable) {
|
|
230
|
+
reason = 'docker unavailable; cannot prove no container uses this data';
|
|
231
|
+
}
|
|
232
|
+
else if (!isSafeServiceDataPath(candidate.projectPath, dataPath)) {
|
|
233
|
+
reason = 'path outside the allowed service data shape';
|
|
234
|
+
}
|
|
235
|
+
else if (!ownershipProven) {
|
|
236
|
+
reason = 'no marker and no registry entry; ownership unproven';
|
|
237
|
+
}
|
|
238
|
+
else if (state === 'live') {
|
|
239
|
+
reason = 'project is live';
|
|
240
|
+
}
|
|
241
|
+
else if (state === 'unknown') {
|
|
242
|
+
reason = 'project state unknown';
|
|
243
|
+
}
|
|
244
|
+
else if (state === 'orphaned') {
|
|
245
|
+
// Data lives inside the project directory, which is gone.
|
|
246
|
+
reason = 'project directory absent';
|
|
247
|
+
}
|
|
248
|
+
else if (mountConflict) {
|
|
249
|
+
reason = `mounted by a running container (${mountConflict})`;
|
|
250
|
+
blockedByRunningBytes += sizeBytes;
|
|
251
|
+
}
|
|
252
|
+
else if (anyContainerRunning) {
|
|
253
|
+
reason = 'a container for this project is still running';
|
|
254
|
+
blockedByRunningBytes += sizeBytes;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
reclaimable = true;
|
|
258
|
+
reason = `project stale, no container running, ownership proven by ${marker ? 'marker' : 'registry'}`;
|
|
259
|
+
}
|
|
260
|
+
dataReports.push({ service, path: dataPath, exists, sizeBytes, reclaimable, reason });
|
|
261
|
+
if (reclaimable) {
|
|
262
|
+
reclaimableDataBytes += sizeBytes;
|
|
263
|
+
dataDirsToRemove.push({
|
|
264
|
+
projectPath: candidate.projectPath,
|
|
265
|
+
service,
|
|
266
|
+
path: dataPath,
|
|
267
|
+
sizeBytes,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Registry entry: safe to drop only once no container remains. For
|
|
272
|
+
// containers created before labels existed, the registry claim is the only
|
|
273
|
+
// thing that can ever identify them again — dropping it while one still
|
|
274
|
+
// exists would make it invisible to prune, `stop -g` and `cleanup -g`,
|
|
275
|
+
// leaving it running forever under `restart: unless-stopped`.
|
|
276
|
+
if (state === 'orphaned' && candidate.registryEntry) {
|
|
277
|
+
if (candidate.containers.length === 0) {
|
|
278
|
+
registryEntriesToRemove.push(candidate.projectPath);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
skipped.push({
|
|
282
|
+
target: candidate.projectPath,
|
|
283
|
+
reason: `project directory is gone but ${candidate.containers.length} container(s) remain; keeping the registry entry so they stay identifiable`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
projects.push({
|
|
288
|
+
projectPath: candidate.projectPath,
|
|
289
|
+
projectName: candidate.registryEntry?.projectName || candidate.projectName,
|
|
290
|
+
state,
|
|
291
|
+
stateReason,
|
|
292
|
+
lastActive,
|
|
293
|
+
inactiveDays,
|
|
294
|
+
projectDirExists,
|
|
295
|
+
registered: !!candidate.registryEntry,
|
|
296
|
+
markerPresent: !!marker,
|
|
297
|
+
containers: containerReports,
|
|
298
|
+
dataDirectories: dataReports,
|
|
299
|
+
totalDataBytes,
|
|
300
|
+
reclaimableDataBytes,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
staleDays: this.staleDays,
|
|
305
|
+
dockerAvailable,
|
|
306
|
+
projects,
|
|
307
|
+
registryEntriesToRemove,
|
|
308
|
+
containersToRemove,
|
|
309
|
+
dataDirsToRemove,
|
|
310
|
+
skipped,
|
|
311
|
+
totals: {
|
|
312
|
+
projects: projects.length,
|
|
313
|
+
totalDataBytes: projects.reduce((sum, project) => sum + project.totalDataBytes, 0),
|
|
314
|
+
reclaimableDataBytes: projects.reduce((sum, project) => sum + project.reclaimableDataBytes, 0),
|
|
315
|
+
blockedByRunningBytes,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Apply a plan, re-verifying every item immediately before it is removed.
|
|
321
|
+
*
|
|
322
|
+
* Any drift between plan and current reality aborts with an error rather than
|
|
323
|
+
* removing something that may since have become live.
|
|
324
|
+
*/
|
|
325
|
+
async applyPlan(planArg) {
|
|
326
|
+
if (!planArg.dockerAvailable && (planArg.containersToRemove.length > 0 || planArg.dataDirsToRemove.length > 0)) {
|
|
327
|
+
throw new Error('Refusing to apply container or data removal without a reachable Docker daemon');
|
|
328
|
+
}
|
|
329
|
+
// Containers first, so their mounts stop protecting the data we then remove.
|
|
330
|
+
if (planArg.containersToRemove.length > 0) {
|
|
331
|
+
const current = await this.docker.inspectMany(planArg.containersToRemove.map((container) => container.id));
|
|
332
|
+
for (const planned of planArg.containersToRemove) {
|
|
333
|
+
const container = current.find((candidate) => candidate.id === planned.id);
|
|
334
|
+
if (!container) {
|
|
335
|
+
throw new Error(`Refusing to remove vanished container: ${planned.name}`);
|
|
336
|
+
}
|
|
337
|
+
if (container.running) {
|
|
338
|
+
throw new Error(`Refusing to remove running container: ${container.name}`);
|
|
339
|
+
}
|
|
340
|
+
const labelOwned = isServiceOwnedByLabels(container.labels);
|
|
341
|
+
const registryOwned = await this.isClaimedByExactlyOneProject(container.name, planned.projectPath);
|
|
342
|
+
if (!labelOwned && !registryOwned) {
|
|
343
|
+
throw new Error(`Refusing to remove container with unproven ownership: ${container.name}`);
|
|
344
|
+
}
|
|
345
|
+
if (!(await this.docker.remove(container.name, false))) {
|
|
346
|
+
throw new Error(`Failed to remove container: ${container.name}`);
|
|
347
|
+
}
|
|
348
|
+
logger.log('ok', `Removed container ${container.name}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Data directories. Cheap, local invariants are re-checked first so an
|
|
352
|
+
// invalid plan is rejected before any Docker call and before anything is
|
|
353
|
+
// deleted; only then is the live mount table consulted.
|
|
354
|
+
if (planArg.dataDirsToRemove.length > 0) {
|
|
355
|
+
const currentProjects = await this.globalRegistry.getAllProjects();
|
|
356
|
+
for (const planned of planArg.dataDirsToRemove) {
|
|
357
|
+
if (!isSafeServiceDataPath(planned.projectPath, planned.path)) {
|
|
358
|
+
throw new Error(`Refusing unsafe service data path: ${planned.path}`);
|
|
359
|
+
}
|
|
360
|
+
const marker = await readServiceDataMarker(planned.projectPath);
|
|
361
|
+
const registered = await this.globalRegistry.isRegistered(planned.projectPath);
|
|
362
|
+
if (!marker && !registered) {
|
|
363
|
+
throw new Error(`Refusing data path with unproven ownership: ${planned.path}`);
|
|
364
|
+
}
|
|
365
|
+
// Re-verify staleness: the project may have been started between
|
|
366
|
+
// planning and applying, which would make this live data.
|
|
367
|
+
const currentEntry = Object.entries(currentProjects).find(([projectPath]) => plugins.path.resolve(projectPath) === planned.projectPath)?.[1];
|
|
368
|
+
if (currentEntry) {
|
|
369
|
+
const inactiveDays = Math.floor((Date.now() - currentEntry.lastActive) / dayInMs);
|
|
370
|
+
if (inactiveDays <= this.staleDays) {
|
|
371
|
+
throw new Error(`Refusing data path for a project that became active: ${planned.path} (${inactiveDays}d)`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const runningMountSources = await this.docker.listRunningMountSources();
|
|
376
|
+
for (const planned of planArg.dataDirsToRemove) {
|
|
377
|
+
const conflict = runningMountSources.find((source) => helpers.pathsOverlap(planned.path, source));
|
|
378
|
+
if (conflict) {
|
|
379
|
+
throw new Error(`Refusing mounted data path: ${planned.path} is used by ${conflict}`);
|
|
380
|
+
}
|
|
381
|
+
if (!(await plugins.smartfs.directory(planned.path).exists())) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
// Escalates to a privileged one-off container when the contents belong to
|
|
385
|
+
// the container user; throws rather than deleting only part of the tree.
|
|
386
|
+
await this.docker.removeDataDirectory(planned.path, serviceImages[planned.service]);
|
|
387
|
+
logger.log('ok', `Removed data directory ${planned.path}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Registry metadata last, so a failure above leaves the claim intact.
|
|
391
|
+
for (const projectPath of planArg.registryEntriesToRemove) {
|
|
392
|
+
if (await plugins.smartfs.directory(projectPath).exists()) {
|
|
393
|
+
throw new Error(`Refusing to unregister an existing project: ${projectPath}`);
|
|
394
|
+
}
|
|
395
|
+
await this.globalRegistry.unregisterProject(projectPath);
|
|
396
|
+
logger.log('ok', `Unregistered ${projectPath}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async isClaimedByExactlyOneProject(containerNameArg, projectPathArg) {
|
|
400
|
+
const projects = await this.globalRegistry.getAllProjects();
|
|
401
|
+
const claims = [];
|
|
402
|
+
for (const [projectPath, project] of Object.entries(projects)) {
|
|
403
|
+
if (Object.values(project.containers).includes(containerNameArg)) {
|
|
404
|
+
claims.push(plugins.path.resolve(projectPath));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return claims.length === 1 && claims[0] === plugins.path.resolve(projectPathArg);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zZXJ2aWNlcHJ1bmVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvbW9kX3NlcnZpY2VzL2NsYXNzZXMuc2VydmljZXBydW5lci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBQ3hDLE9BQU8sRUFBRSxlQUFlLEVBQThCLE1BQU0sOEJBQThCLENBQUM7QUFDM0YsT0FBTyxFQUFFLGNBQWMsRUFBMkIsTUFBTSw2QkFBNkIsQ0FBQztBQUN0RixPQUFPLEVBQ0wsdUJBQXVCLEVBQ3ZCLHFCQUFxQixFQUNyQixzQkFBc0IsRUFDdEIscUJBQXFCLEVBQ3JCLGFBQWEsRUFDYixZQUFZLEVBQ1osZ0JBQWdCLEdBRWpCLE1BQU0sZ0NBQWdDLENBQUM7QUFDeEMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBRS9DLDhFQUE4RTtBQUM5RSxNQUFNLENBQUMsTUFBTSxnQkFBZ0IsR0FBRyxFQUFFLENBQUM7QUFnRm5DLE1BQU0sT0FBTyxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztBQUVwQzs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxPQUFPLGFBQWE7SUFDaEIsTUFBTSxDQUFrQjtJQUN4QixjQUFjLENBQWlCO0lBQy9CLFNBQVMsQ0FBUztJQUUxQixZQUFZLGFBQW9DLEVBQUU7UUFDaEQsSUFBSSxDQUFDLE1BQU0sR0FBRyxJQUFJLGVBQWUsRUFBRSxDQUFDO1FBQ3BDLElBQUksQ0FBQyxjQUFjLEdBQUcsY0FBYyxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQ25ELElBQUksQ0FBQyxTQUFTLEdBQUcsVUFBVSxDQUFDLFNBQVMsSUFBSSxnQkFBZ0IsQ0FBQztJQUM1RCxDQUFDO0lBRUQ7O09BRUc7SUFDSSxLQUFLLENBQUMsVUFBVTtRQUNyQixN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
|
|
@@ -18,6 +18,21 @@ export declare const printHeader: (title: string) => void;
|
|
|
18
18
|
* Format bytes to human readable string
|
|
19
19
|
*/
|
|
20
20
|
export declare const formatBytes: (bytes: number) => string;
|
|
21
|
+
/**
|
|
22
|
+
* True when either path contains the other, or they are the same path.
|
|
23
|
+
*
|
|
24
|
+
* Used to decide whether a deletion candidate collides with a directory a
|
|
25
|
+
* running container has mounted. Containment in *either* direction counts: a
|
|
26
|
+
* parent mount protects its children, and a child mount protects its parent.
|
|
27
|
+
*/
|
|
28
|
+
export declare const pathsOverlap: (firstPathArg: string, secondPathArg: string) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Total size in bytes of a directory tree.
|
|
31
|
+
*
|
|
32
|
+
* Symlinks are counted by their own size and never followed, so a symlink
|
|
33
|
+
* inside a data directory can neither inflate the report nor walk out of it.
|
|
34
|
+
*/
|
|
35
|
+
export declare const getDirectorySize: (directoryPathArg: string, maxDepthArg?: number) => Promise<number>;
|
|
21
36
|
/**
|
|
22
37
|
* Get the local network IP address
|
|
23
38
|
*/
|
|
@@ -77,6 +77,62 @@ export const formatBytes = (bytes) => {
|
|
|
77
77
|
}
|
|
78
78
|
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
79
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* True when either path contains the other, or they are the same path.
|
|
82
|
+
*
|
|
83
|
+
* Used to decide whether a deletion candidate collides with a directory a
|
|
84
|
+
* running container has mounted. Containment in *either* direction counts: a
|
|
85
|
+
* parent mount protects its children, and a child mount protects its parent.
|
|
86
|
+
*/
|
|
87
|
+
export const pathsOverlap = (firstPathArg, secondPathArg) => {
|
|
88
|
+
const firstPath = plugins.path.resolve(firstPathArg);
|
|
89
|
+
const secondPath = plugins.path.resolve(secondPathArg);
|
|
90
|
+
const contains = (containerPath, candidatePath) => {
|
|
91
|
+
const relative = plugins.path.relative(containerPath, candidatePath);
|
|
92
|
+
if (relative === '') {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return !!relative && !relative.startsWith('..') && !plugins.path.isAbsolute(relative);
|
|
96
|
+
};
|
|
97
|
+
return contains(firstPath, secondPath) || contains(secondPath, firstPath);
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Total size in bytes of a directory tree.
|
|
101
|
+
*
|
|
102
|
+
* Symlinks are counted by their own size and never followed, so a symlink
|
|
103
|
+
* inside a data directory can neither inflate the report nor walk out of it.
|
|
104
|
+
*/
|
|
105
|
+
export const getDirectorySize = async (directoryPathArg, maxDepthArg = 64) => {
|
|
106
|
+
let total = 0;
|
|
107
|
+
const walk = async (currentPath, depth) => {
|
|
108
|
+
// Bind mounts inside a data directory are user-controlled and can form a
|
|
109
|
+
// cycle; a depth cap keeps this from recursing until it exhausts memory.
|
|
110
|
+
if (depth > maxDepthArg) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const entries = await plugins.fs
|
|
114
|
+
.readdir(currentPath, { withFileTypes: true })
|
|
115
|
+
.catch(() => []);
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const entryPath = plugins.path.join(currentPath, entry.name);
|
|
118
|
+
// isDirectory() here is lstat-based, so symlinks are counted by their own
|
|
119
|
+
// size and never followed.
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
await walk(entryPath, depth + 1);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const stats = await plugins.fs.lstat(entryPath);
|
|
126
|
+
total += stats.size;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// entry disappeared between readdir and lstat; ignore
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
await walk(plugins.path.resolve(directoryPathArg), 0);
|
|
134
|
+
return total;
|
|
135
|
+
};
|
|
80
136
|
/**
|
|
81
137
|
* Get the local network IP address
|
|
82
138
|
*/
|
|
@@ -109,4 +165,4 @@ export const getLocalNetworkIp = async () => {
|
|
|
109
165
|
// Last resort: localhost
|
|
110
166
|
return 'localhost';
|
|
111
167
|
};
|
|
112
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
168
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGVscGVycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL21vZF9zZXJ2aWNlcy9oZWxwZXJzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxLQUFLLEdBQUcsTUFBTSxLQUFLLENBQUM7QUFDM0IsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBRS9DOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sZUFBZSxHQUFHLEtBQUssRUFBRSxJQUFZLEVBQW9CLEVBQUU7SUFDdEUsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFO1FBQzdCLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxZQUFZLEVBQUUsQ0FBQztRQUVsQyxNQUFNLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxHQUFHLEVBQUU7WUFDeEIsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ2pCLENBQUMsQ0FBQyxDQUFDO1FBRUgsTUFBTSxDQUFDLElBQUksQ0FBQyxXQUFXLEVBQUUsR0FBRyxFQUFFO1lBQzVCLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQztZQUNmLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNoQixDQUFDLENBQUMsQ0FBQztRQUVILE1BQU0sQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFLFNBQVMsQ0FBQyxDQUFDO0lBQ2pDLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDO0FBRUY7O0dBRUc7QUFDSCxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxLQUFLLElBQXFCLEVBQUU7SUFDaEUsTUFBTSxXQUFXLEdBQUcsR0FBRyxDQUFDO0lBRXhCLEtBQUssSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsR0FBRyxXQUFXLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQztRQUNyQyxNQUFNLElBQUksR0FBRyxJQUFJLENB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
|