docker-storage-gc 4.0.0 → 4.0.1
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/.versionbot/CHANGELOG.yml +19 -1
- package/CHANGELOG.md +6 -0
- package/build/docker-event-stream.d.ts +1 -2
- package/build/docker-event-stream.js +9 -5
- package/build/docker-event-stream.js.map +1 -1
- package/build/docker-image-tree.d.ts +1 -2
- package/build/docker-image-tree.js +6 -6
- package/build/docker-image-tree.js.map +1 -1
- package/build/docker.d.ts +1 -1
- package/build/docker.js +5 -6
- package/build/docker.js.map +1 -1
- package/build/index.js +67 -74
- package/build/index.js.map +1 -1
- package/lib/docker-event-stream.ts +30 -32
- package/lib/docker-image-tree.ts +7 -6
- package/lib/docker.ts +5 -6
- package/lib/index.ts +87 -95
- package/package.json +2 -2
- package/test/docker-event-stream.ts +26 -30
- package/test/docker-image-tree.ts +102 -105
- package/test/index.ts +127 -142
package/lib/index.ts
CHANGED
|
@@ -102,42 +102,33 @@ export default class DockerGC {
|
|
|
102
102
|
this.host = hostname;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
public setDocker(hostObj: Docker.DockerOptions): Promise<void> {
|
|
105
|
+
public async setDocker(hostObj: Docker.DockerOptions): Promise<void> {
|
|
106
106
|
this.currentMtimes = {};
|
|
107
|
-
hostObj = _.defaults({ Promise: Bluebird }, hostObj);
|
|
108
107
|
this.dockerProgress = new DockerProgress({
|
|
109
108
|
docker: new Docker(hostObj),
|
|
110
109
|
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
));
|
|
130
|
-
})
|
|
131
|
-
.then(() => {
|
|
132
|
-
// noop
|
|
133
|
-
});
|
|
110
|
+
const docker = getDocker(hostObj);
|
|
111
|
+
// Docker info can take a while so do it here,
|
|
112
|
+
// and don't wait on the results
|
|
113
|
+
this.docker = docker;
|
|
114
|
+
await (this.baseImagePromise = this.getDaemonArchitecture().then((arch) => {
|
|
115
|
+
switch (arch) {
|
|
116
|
+
case 'arm':
|
|
117
|
+
return 'arm32v6/alpine:3.6';
|
|
118
|
+
case 'arm64':
|
|
119
|
+
return 'arm64v8/alpine:3.6';
|
|
120
|
+
case 'amd64':
|
|
121
|
+
return 'alpine:3.6';
|
|
122
|
+
default:
|
|
123
|
+
throw new Error('Could not detect architecture of remote host');
|
|
124
|
+
}
|
|
125
|
+
}));
|
|
134
126
|
}
|
|
135
127
|
|
|
136
|
-
public setupMtimeStream(): Promise<void> {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
});
|
|
128
|
+
public async setupMtimeStream(): Promise<void> {
|
|
129
|
+
const stream = await dockerMtimeStream(this.docker);
|
|
130
|
+
stream.on('data', (layerMtimes: LayerMtimes) => {
|
|
131
|
+
this.currentMtimes = layerMtimes;
|
|
141
132
|
});
|
|
142
133
|
}
|
|
143
134
|
|
|
@@ -165,60 +156,56 @@ export default class DockerGC {
|
|
|
165
156
|
removalType: 'tag' | 'digest' | 'id',
|
|
166
157
|
): Promise<void> | undefined {
|
|
167
158
|
if (attributes.length > 0) {
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.getImage(attribute)
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
});
|
|
178
|
-
}).then(() => {
|
|
179
|
-
// noop
|
|
180
|
-
});
|
|
159
|
+
return (async () => {
|
|
160
|
+
for (const attribute of attributes) {
|
|
161
|
+
console.log(
|
|
162
|
+
`[GC (${this.host}] Removing image : ${attribute} (id: ${image.id})`,
|
|
163
|
+
);
|
|
164
|
+
await this.docker.getImage(attribute).remove({ noprune: true });
|
|
165
|
+
this.metrics.emit('imageRemoved', removalType);
|
|
166
|
+
}
|
|
167
|
+
})();
|
|
181
168
|
}
|
|
182
169
|
}
|
|
183
170
|
|
|
184
|
-
public garbageCollect(
|
|
171
|
+
public async garbageCollect(
|
|
185
172
|
reclaimSpace: number,
|
|
186
173
|
attemptAll = false,
|
|
187
174
|
): Promise<void> {
|
|
188
175
|
let err: any;
|
|
189
176
|
const startTime = process.hrtime();
|
|
190
177
|
this.metrics.emit('spaceReclaimed', reclaimSpace);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
})
|
|
209
|
-
.then(() => {
|
|
210
|
-
recordGcRunTime(startTime, this.metrics);
|
|
211
|
-
if (err != null) {
|
|
212
|
-
throw err;
|
|
178
|
+
|
|
179
|
+
const tree = await dockerImageTree(this.docker, this.currentMtimes);
|
|
180
|
+
const images = await getImagesToRemove(tree, reclaimSpace, this.metrics);
|
|
181
|
+
for (const image of images) {
|
|
182
|
+
try {
|
|
183
|
+
await this.removeImage(image);
|
|
184
|
+
} catch (e: any) {
|
|
185
|
+
this.metrics.emit('imageRemovalError', e.statusCode);
|
|
186
|
+
console.log(`[GC ${this.host}]: Failed to remove image: `, image);
|
|
187
|
+
console.log(e);
|
|
188
|
+
if (attemptAll) {
|
|
189
|
+
err ??= e;
|
|
190
|
+
} else {
|
|
191
|
+
recordGcRunTime(startTime, this.metrics);
|
|
192
|
+
throw e;
|
|
213
193
|
}
|
|
214
|
-
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
recordGcRunTime(startTime, this.metrics);
|
|
197
|
+
if (err != null) {
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
215
200
|
}
|
|
216
201
|
|
|
217
202
|
private getOutput(image: string, command: string[]): Promise<string> {
|
|
218
|
-
return Bluebird.using(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
.
|
|
203
|
+
return Bluebird.using(
|
|
204
|
+
this.runDisposer(image, command),
|
|
205
|
+
async (container) => {
|
|
206
|
+
const logs = await container.logs({ stdout: true, follow: true });
|
|
207
|
+
return await streamToString(logs);
|
|
208
|
+
},
|
|
222
209
|
);
|
|
223
210
|
}
|
|
224
211
|
|
|
@@ -235,38 +222,43 @@ export default class DockerGC {
|
|
|
235
222
|
);
|
|
236
223
|
return Bluebird.resolve(
|
|
237
224
|
containerPromise.then(([, container]) => container),
|
|
238
|
-
).disposer((container) =>
|
|
225
|
+
).disposer(async (container) => {
|
|
226
|
+
await container.wait();
|
|
227
|
+
await container.remove();
|
|
228
|
+
});
|
|
239
229
|
}
|
|
240
230
|
|
|
241
|
-
public getDaemonFreeSpace(): Promise<{
|
|
231
|
+
public async getDaemonFreeSpace(): Promise<{
|
|
242
232
|
used: number;
|
|
243
233
|
total: number;
|
|
244
234
|
free: number;
|
|
245
235
|
}> {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
236
|
+
const baseImage = await this.baseImagePromise;
|
|
237
|
+
|
|
238
|
+
// Ensure the image is available (if it is this is essentially a no-op)
|
|
239
|
+
await this.dockerProgress.pull(baseImage, _.noop);
|
|
240
|
+
|
|
241
|
+
const spaceStr = await this.getOutput(baseImage, [
|
|
242
|
+
'/bin/df',
|
|
243
|
+
'-B',
|
|
244
|
+
'1',
|
|
245
|
+
'/',
|
|
246
|
+
]);
|
|
247
|
+
// First split the lines, as we're only interested in the second one
|
|
248
|
+
const lines = spaceStr.trim().split(/\r?\n/);
|
|
249
|
+
if (lines.length !== 2) {
|
|
250
|
+
throw new Error('Coult not parse df output');
|
|
251
|
+
}
|
|
260
252
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
});
|
|
253
|
+
const parts = lines[1].split(/\s+/);
|
|
254
|
+
const total = parseInt(parts[1], 10);
|
|
255
|
+
const used = parseInt(parts[2], 10);
|
|
256
|
+
const free = parseInt(parts[3], 10);
|
|
257
|
+
return { used, total, free };
|
|
267
258
|
}
|
|
268
259
|
|
|
269
|
-
private getDaemonArchitecture() {
|
|
270
|
-
|
|
260
|
+
private async getDaemonArchitecture() {
|
|
261
|
+
const { Arch } = await this.docker.version();
|
|
262
|
+
return Arch;
|
|
271
263
|
}
|
|
272
264
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docker-storage-gc",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"lint": "balena-lint -t tsconfig.dev.json -e ts -e js lib test tools",
|
|
@@ -39,6 +39,6 @@
|
|
|
39
39
|
"typescript": "^5.3.2"
|
|
40
40
|
},
|
|
41
41
|
"versionist": {
|
|
42
|
-
"publishedAt": "2023-12-
|
|
42
|
+
"publishedAt": "2023-12-05T22:24:21.230Z"
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -11,36 +11,32 @@ describe('parseEventStream', function () {
|
|
|
11
11
|
// TODO
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
it('should return updated mtimes', () =>
|
|
15
|
-
getDocker({})
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
new Promise<LayerMtimes>(function (resolve, reject) {
|
|
20
|
-
let mtimes: LayerMtimes;
|
|
14
|
+
it('should return updated mtimes', async () => {
|
|
15
|
+
const docker = await getDocker({});
|
|
16
|
+
const streamParser = await parseEventStream(docker);
|
|
17
|
+
const data = await new Promise<LayerMtimes>(function (resolve, reject) {
|
|
18
|
+
let mtimes: LayerMtimes;
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
return fs
|
|
21
|
+
.createReadStream(__dirname + '/fixtures/docker-events.json')
|
|
22
|
+
.pipe(streamParser)
|
|
23
|
+
.on('error', reject)
|
|
24
|
+
.pipe(es.mapSync(($data: LayerMtimes) => (mtimes = $data)))
|
|
25
|
+
.on('end', () => resolve(mtimes))
|
|
26
|
+
.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
expect(data)
|
|
29
|
+
.to.have.property('busybox:latest')
|
|
30
|
+
.that.equals(1448576072937294800);
|
|
31
|
+
expect(data)
|
|
32
|
+
.to.have.property(
|
|
33
|
+
'sha256:6d41a4a0bf8168363e29da8a5ecbf3cd6c37e3f5a043decd5e7da6e427ba869c',
|
|
30
34
|
)
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
.that.equals(1448576073085559800);
|
|
40
|
-
expect(data)
|
|
41
|
-
.to.have.property(
|
|
42
|
-
'sha256:9a61b6b1315e6b457c31a03346ab94486a2f5397f4a82219bee01eead1c34c2e',
|
|
43
|
-
)
|
|
44
|
-
.that.equals(1448576073203895800);
|
|
45
|
-
}));
|
|
35
|
+
.that.equals(1448576073085559800);
|
|
36
|
+
expect(data)
|
|
37
|
+
.to.have.property(
|
|
38
|
+
'sha256:9a61b6b1315e6b457c31a03346ab94486a2f5397f4a82219bee01eead1c34c2e',
|
|
39
|
+
)
|
|
40
|
+
.that.equals(1448576073203895800);
|
|
41
|
+
});
|
|
46
42
|
});
|
|
@@ -7,22 +7,20 @@ import { LayerMtimes, parseEventStream } from '../build/docker-event-stream';
|
|
|
7
7
|
import { createTree } from '../build/docker-image-tree';
|
|
8
8
|
import { getDocker } from '../build/docker';
|
|
9
9
|
|
|
10
|
-
const getLayerMtimes = () =>
|
|
11
|
-
getDocker({})
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}),
|
|
25
|
-
);
|
|
10
|
+
const getLayerMtimes = async () => {
|
|
11
|
+
const docker = await getDocker({});
|
|
12
|
+
const streamParser = await parseEventStream(docker);
|
|
13
|
+
return await new Promise<LayerMtimes>(function (resolve, reject) {
|
|
14
|
+
let mtimes: LayerMtimes;
|
|
15
|
+
return fs
|
|
16
|
+
.createReadStream(__dirname + '/fixtures/docker-events.json')
|
|
17
|
+
.pipe(streamParser)
|
|
18
|
+
.on('error', reject)
|
|
19
|
+
.pipe(es.mapSync((data: LayerMtimes) => (mtimes = data)))
|
|
20
|
+
.on('end', () => resolve(mtimes))
|
|
21
|
+
.on('error', reject);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
26
24
|
|
|
27
25
|
describe('createTree', function () {
|
|
28
26
|
it.skip('should work with empty input', function () {
|
|
@@ -40,98 +38,97 @@ describe('createTree', function () {
|
|
|
40
38
|
assert: { type: 'json' },
|
|
41
39
|
})
|
|
42
40
|
).default as ContainerInfo[];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
},
|
|
41
|
+
const mtimes = await getLayerMtimes();
|
|
42
|
+
tk.freeze(Date.UTC(2016, 0, 1));
|
|
43
|
+
const tree = createTree(images, containers, mtimes);
|
|
44
|
+
tk.reset();
|
|
45
|
+
const output = {
|
|
46
|
+
id: '0000000000000000000000000000000000000000000000000000000000000000',
|
|
47
|
+
size: 0,
|
|
48
|
+
repoTags: [],
|
|
49
|
+
repoDigests: [],
|
|
50
|
+
mtime: 1451606400000000000,
|
|
51
|
+
isUsedByAContainer: false,
|
|
52
|
+
children: {
|
|
53
|
+
'sha256:6d15899cef812e2876b9d5d43d4cd863eda7b278f7b52d00975f6a9a8e817c74':
|
|
54
|
+
{
|
|
55
|
+
id: 'sha256:6d15899cef812e2876b9d5d43d4cd863eda7b278f7b52d00975f6a9a8e817c74',
|
|
56
|
+
size: 125151141,
|
|
57
|
+
repoTags: [],
|
|
58
|
+
repoDigests: [],
|
|
59
|
+
mtime: 1451606400000000000,
|
|
60
|
+
isUsedByAContainer: false,
|
|
61
|
+
children: {
|
|
62
|
+
'sha256:e53bd4df04f86919156c4510cdc6e6c9491ec8ec226381d36aca573b46bbbbbc':
|
|
63
|
+
{
|
|
64
|
+
id: 'sha256:e53bd4df04f86919156c4510cdc6e6c9491ec8ec226381d36aca573b46bbbbbc',
|
|
65
|
+
size: 0,
|
|
66
|
+
repoTags: ['project1'],
|
|
67
|
+
repoDigests: [],
|
|
68
|
+
mtime: 1451606400000000000,
|
|
69
|
+
isUsedByAContainer: false,
|
|
70
|
+
children: {
|
|
71
|
+
'sha256:6d41a4a0bf8168363e29da8a5ecbf3cd6c37e3f5a043decd5e7da6e427ba869c':
|
|
72
|
+
{
|
|
73
|
+
id: 'sha256:6d41a4a0bf8168363e29da8a5ecbf3cd6c37e3f5a043decd5e7da6e427ba869c',
|
|
74
|
+
size: 330389,
|
|
75
|
+
repoTags: ['project2'],
|
|
76
|
+
repoDigests: [],
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
|
|
78
|
+
mtime: 1448576073085559863,
|
|
79
|
+
isUsedByAContainer: false,
|
|
80
|
+
children: {
|
|
81
|
+
'sha256:80dc79d29cd8618e678da508fc32f7289e6f72defb534f3f287731b1f8b355ea':
|
|
82
|
+
{
|
|
83
|
+
id: 'sha256:80dc79d29cd8618e678da508fc32f7289e6f72defb534f3f287731b1f8b355ea',
|
|
84
|
+
size: 98872,
|
|
85
|
+
repoTags: [],
|
|
86
|
+
repoDigests: [],
|
|
87
|
+
mtime: 1451606400000000000,
|
|
88
|
+
isUsedByAContainer: false,
|
|
89
|
+
children: {},
|
|
90
|
+
},
|
|
94
91
|
},
|
|
95
|
-
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
'sha256:902b87aaaec929e80541486828959f14fa061f529ad7f37ab300d4ef9f3a0dbf':
|
|
100
|
-
{
|
|
101
|
-
id: 'sha256:902b87aaaec929e80541486828959f14fa061f529ad7f37ab300d4ef9f3a0dbf',
|
|
102
|
-
size: 125151141,
|
|
103
|
-
repoTags: [],
|
|
104
|
-
repoDigests: [],
|
|
105
|
-
mtime: 1451606400000000000,
|
|
106
|
-
isUsedByAContainer: false,
|
|
107
|
-
children: {
|
|
108
|
-
'sha256:9a61b6b1315e6b457c31a03346ab94486a2f5397f4a82219bee01eead1c34c2e':
|
|
109
|
-
{
|
|
110
|
-
id: 'sha256:9a61b6b1315e6b457c31a03346ab94486a2f5397f4a82219bee01eead1c34c2e',
|
|
111
|
-
size: 0,
|
|
112
|
-
repoTags: ['resin/project3'],
|
|
113
|
-
repoDigests: [],
|
|
114
|
-
mtime: 1448576073203895800,
|
|
115
|
-
isUsedByAContainer: false,
|
|
116
|
-
children: {},
|
|
92
|
+
},
|
|
117
93
|
},
|
|
118
|
-
|
|
94
|
+
},
|
|
119
95
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
96
|
+
},
|
|
97
|
+
'sha256:902b87aaaec929e80541486828959f14fa061f529ad7f37ab300d4ef9f3a0dbf':
|
|
98
|
+
{
|
|
99
|
+
id: 'sha256:902b87aaaec929e80541486828959f14fa061f529ad7f37ab300d4ef9f3a0dbf',
|
|
100
|
+
size: 125151141,
|
|
101
|
+
repoTags: [],
|
|
102
|
+
repoDigests: [],
|
|
103
|
+
mtime: 1451606400000000000,
|
|
104
|
+
isUsedByAContainer: false,
|
|
105
|
+
children: {
|
|
106
|
+
'sha256:9a61b6b1315e6b457c31a03346ab94486a2f5397f4a82219bee01eead1c34c2e':
|
|
107
|
+
{
|
|
108
|
+
id: 'sha256:9a61b6b1315e6b457c31a03346ab94486a2f5397f4a82219bee01eead1c34c2e',
|
|
109
|
+
size: 0,
|
|
110
|
+
repoTags: ['resin/project3'],
|
|
111
|
+
repoDigests: [],
|
|
112
|
+
mtime: 1448576073203895800,
|
|
113
|
+
isUsedByAContainer: false,
|
|
114
|
+
children: {},
|
|
115
|
+
},
|
|
131
116
|
},
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
117
|
+
},
|
|
118
|
+
'sha256:5b0d59026729b68570d99bc4f3f7c31a2e4f2a5736435641565d93e7c25bd2c3':
|
|
119
|
+
{
|
|
120
|
+
id: 'sha256:5b0d59026729b68570d99bc4f3f7c31a2e4f2a5736435641565d93e7c25bd2c3',
|
|
121
|
+
size: 125151141,
|
|
122
|
+
repoTags: ['busybox:latest'],
|
|
123
|
+
repoDigests: [
|
|
124
|
+
'sha256:a8cf7ff6367c2afa2a90acd081b484cbded349a7076e7bdf37a05279f276bc12',
|
|
125
|
+
],
|
|
126
|
+
mtime: 1448576072937294800,
|
|
127
|
+
isUsedByAContainer: true,
|
|
128
|
+
children: {},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
expect(tree).to.deep.equal(output);
|
|
136
133
|
});
|
|
137
134
|
});
|