docker-storage-gc 3.5.12 → 4.0.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/.versionbot/CHANGELOG.yml +19 -1
- package/CHANGELOG.md +6 -0
- package/build/docker-event-stream.d.ts +8 -0
- package/build/docker-event-stream.js +80 -49
- package/build/docker-event-stream.js.map +1 -0
- package/build/docker-image-tree.d.ts +15 -0
- package/build/docker-image-tree.js +57 -76
- package/build/docker-image-tree.js.map +1 -0
- package/build/docker.d.ts +2 -0
- package/build/docker.js +21 -25
- package/build/docker.js.map +1 -0
- package/build/index.d.ts +33 -0
- package/build/index.js +174 -229
- package/build/index.js.map +1 -0
- package/lib/docker-event-stream.ts +93 -0
- package/lib/docker-image-tree.ts +94 -0
- package/lib/docker.ts +17 -0
- package/lib/index.ts +272 -0
- package/package.json +21 -13
- package/test/docker-event-stream.ts +46 -0
- package/test/docker-image-tree.ts +137 -0
- package/test/index.ts +228 -0
- package/tools/graphviz.ts +39 -0
- package/tsconfig.dev.json +13 -0
- package/tsconfig.json +22 -0
- package/index.d.ts +0 -23
- package/lib/docker-event-stream.coffee +0 -46
- package/lib/docker-image-tree.coffee +0 -56
- package/lib/docker.coffee +0 -13
- package/lib/index.coffee +0 -168
- package/test/docker-event-stream.coffee +0 -30
- package/test/docker-image-tree.coffee +0 -124
- package/test/index.coffee +0 -208
- package/tools/graphviz.coffee +0 -24
package/lib/index.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import Bluebird, { Disposer } from 'bluebird';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import { EventEmitter } from 'eventemitter3';
|
|
4
|
+
import { DockerProgress } from 'docker-progress';
|
|
5
|
+
import Docker from 'dockerode';
|
|
6
|
+
import { LayerMtimes, dockerMtimeStream } from './docker-event-stream';
|
|
7
|
+
import { ImageNode, dockerImageTree } from './docker-image-tree';
|
|
8
|
+
import { getDocker } from './docker';
|
|
9
|
+
|
|
10
|
+
interface Events {
|
|
11
|
+
numberImagesToRemove(n: number): void;
|
|
12
|
+
gcRunTime(duration: number): void;
|
|
13
|
+
imageRemoved(removalType: string): void;
|
|
14
|
+
spaceReclaimed(reclaimSpace: number): void;
|
|
15
|
+
imageRemovalError(statusCode: string): void;
|
|
16
|
+
}
|
|
17
|
+
type Metrics = EventEmitter<Events>;
|
|
18
|
+
|
|
19
|
+
interface RemovableImageNode extends ImageNode {
|
|
20
|
+
removed?: true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const getUnusedTreeLeafs = function (
|
|
24
|
+
tree: RemovableImageNode,
|
|
25
|
+
result: RemovableImageNode[] = [],
|
|
26
|
+
) {
|
|
27
|
+
if (!tree.removed) {
|
|
28
|
+
const children = _(tree.children)
|
|
29
|
+
.values()
|
|
30
|
+
.filter(_.negate(_.property('removed')))
|
|
31
|
+
.value();
|
|
32
|
+
if (children.length === 0 && !tree.isUsedByAContainer) {
|
|
33
|
+
result.push(tree);
|
|
34
|
+
} else {
|
|
35
|
+
for (const child of children) {
|
|
36
|
+
getUnusedTreeLeafs(child, result);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getImagesToRemove = function (
|
|
44
|
+
tree: RemovableImageNode,
|
|
45
|
+
reclaimSpace: number,
|
|
46
|
+
metrics: Metrics,
|
|
47
|
+
) {
|
|
48
|
+
// Removes the oldest, largest leafs first.
|
|
49
|
+
// This should avoid trying to remove images with children.
|
|
50
|
+
tree = _.clone(tree);
|
|
51
|
+
const result = [];
|
|
52
|
+
let size = 0;
|
|
53
|
+
while (size < reclaimSpace) {
|
|
54
|
+
const leafs = _.orderBy(
|
|
55
|
+
getUnusedTreeLeafs(tree),
|
|
56
|
+
['mtime', 'size'],
|
|
57
|
+
['asc', 'desc'],
|
|
58
|
+
);
|
|
59
|
+
if (leafs.length === 0) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const leaf = leafs[0];
|
|
63
|
+
if (leaf !== tree) {
|
|
64
|
+
// don't remove the tree root
|
|
65
|
+
result.push(leaf);
|
|
66
|
+
size += leaf.size;
|
|
67
|
+
}
|
|
68
|
+
leaf.removed = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
metrics.emit('numberImagesToRemove', result.length);
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const streamToString = (stream: NodeJS.ReadableStream) =>
|
|
76
|
+
new Promise<string>(function (resolve, reject) {
|
|
77
|
+
const chunks: Buffer[] = [];
|
|
78
|
+
stream
|
|
79
|
+
.on('error', reject)
|
|
80
|
+
.on('data', (chunk) => chunks.push(chunk))
|
|
81
|
+
.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const recordGcRunTime = function (
|
|
85
|
+
t0: ReturnType<NodeJS.HRTime>,
|
|
86
|
+
metrics: Metrics,
|
|
87
|
+
) {
|
|
88
|
+
const dt = process.hrtime(t0);
|
|
89
|
+
const duration = dt[0] * 1000 + dt[1] / 1e6;
|
|
90
|
+
metrics.emit('gcRunTime', duration);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default class DockerGC {
|
|
94
|
+
public metrics: Metrics = new EventEmitter<Events>();
|
|
95
|
+
private host = 'unknown';
|
|
96
|
+
private docker: Docker;
|
|
97
|
+
private dockerProgress: DockerProgress;
|
|
98
|
+
private currentMtimes: LayerMtimes = {};
|
|
99
|
+
private baseImagePromise: Promise<string>;
|
|
100
|
+
|
|
101
|
+
public setHostname(hostname: string): void {
|
|
102
|
+
this.host = hostname;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public setDocker(hostObj: Docker.DockerOptions): Promise<void> {
|
|
106
|
+
this.currentMtimes = {};
|
|
107
|
+
hostObj = _.defaults({ Promise: Bluebird }, hostObj);
|
|
108
|
+
this.dockerProgress = new DockerProgress({
|
|
109
|
+
docker: new Docker(hostObj),
|
|
110
|
+
});
|
|
111
|
+
return getDocker(hostObj)
|
|
112
|
+
.then((docker) => {
|
|
113
|
+
// Docker info can take a while so do it here,
|
|
114
|
+
// and don't wait on the results
|
|
115
|
+
this.docker = docker;
|
|
116
|
+
return (this.baseImagePromise = this.getDaemonArchitecture().then(
|
|
117
|
+
function (arch) {
|
|
118
|
+
switch (arch) {
|
|
119
|
+
case 'arm':
|
|
120
|
+
return 'arm32v6/alpine:3.6';
|
|
121
|
+
case 'arm64':
|
|
122
|
+
return 'arm64v8/alpine:3.6';
|
|
123
|
+
case 'amd64':
|
|
124
|
+
return 'alpine:3.6';
|
|
125
|
+
default:
|
|
126
|
+
throw new Error('Could not detect architecture of remote host');
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
));
|
|
130
|
+
})
|
|
131
|
+
.then(() => {
|
|
132
|
+
// noop
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public setupMtimeStream(): Promise<void> {
|
|
137
|
+
return dockerMtimeStream(this.docker).then((stream) => {
|
|
138
|
+
stream.on('data', (layerMtimes: LayerMtimes) => {
|
|
139
|
+
this.currentMtimes = layerMtimes;
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private removeImage(image: RemovableImageNode) {
|
|
145
|
+
return (
|
|
146
|
+
this.tryRemoveImageBy(image, image.repoTags, 'tag') ||
|
|
147
|
+
this.tryRemoveImageBy(image, image.repoDigests, 'digest') ||
|
|
148
|
+
this.tryRemoveImageBy(image, [image.id], 'id')
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private tryRemoveImageBy(
|
|
153
|
+
image: RemovableImageNode,
|
|
154
|
+
attributes: [string],
|
|
155
|
+
removalType: 'tag' | 'digest' | 'id',
|
|
156
|
+
): Promise<void>;
|
|
157
|
+
private tryRemoveImageBy(
|
|
158
|
+
image: RemovableImageNode,
|
|
159
|
+
attributes: string[],
|
|
160
|
+
removalType: 'tag' | 'digest' | 'id',
|
|
161
|
+
): Promise<void> | undefined;
|
|
162
|
+
private tryRemoveImageBy(
|
|
163
|
+
image: RemovableImageNode,
|
|
164
|
+
attributes: string[],
|
|
165
|
+
removalType: 'tag' | 'digest' | 'id',
|
|
166
|
+
): Promise<void> | undefined {
|
|
167
|
+
if (attributes.length > 0) {
|
|
168
|
+
return Bluebird.each(attributes, (attribute) => {
|
|
169
|
+
console.log(
|
|
170
|
+
`[GC (${this.host}] Removing image : ${attribute} (id: ${image.id})`,
|
|
171
|
+
);
|
|
172
|
+
return this.docker
|
|
173
|
+
.getImage(attribute)
|
|
174
|
+
.remove({ noprune: true })
|
|
175
|
+
.then(() => {
|
|
176
|
+
this.metrics.emit('imageRemoved', removalType);
|
|
177
|
+
});
|
|
178
|
+
}).then(() => {
|
|
179
|
+
// noop
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public garbageCollect(
|
|
185
|
+
reclaimSpace: number,
|
|
186
|
+
attemptAll = false,
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
let err: any;
|
|
189
|
+
const startTime = process.hrtime();
|
|
190
|
+
this.metrics.emit('spaceReclaimed', reclaimSpace);
|
|
191
|
+
return dockerImageTree(this.docker, this.currentMtimes)
|
|
192
|
+
.then((tree) => {
|
|
193
|
+
return getImagesToRemove(tree, reclaimSpace, this.metrics);
|
|
194
|
+
})
|
|
195
|
+
.each((image) => {
|
|
196
|
+
return this.removeImage(image).catch((e) => {
|
|
197
|
+
this.metrics.emit('imageRemovalError', e.statusCode);
|
|
198
|
+
console.log(`[GC ${this.host}]: Failed to remove image: `, image);
|
|
199
|
+
console.log(e);
|
|
200
|
+
if (attemptAll) {
|
|
201
|
+
err ??= e;
|
|
202
|
+
return;
|
|
203
|
+
} else {
|
|
204
|
+
recordGcRunTime(startTime, this.metrics);
|
|
205
|
+
throw e;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
})
|
|
209
|
+
.then(() => {
|
|
210
|
+
recordGcRunTime(startTime, this.metrics);
|
|
211
|
+
if (err != null) {
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private getOutput(image: string, command: string[]): Promise<string> {
|
|
218
|
+
return Bluebird.using(this.runDisposer(image, command), (container) =>
|
|
219
|
+
container
|
|
220
|
+
.logs({ stdout: true, follow: true })
|
|
221
|
+
.then((logs) => streamToString(logs)),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private runDisposer(
|
|
226
|
+
image: string,
|
|
227
|
+
command: string[],
|
|
228
|
+
): Disposer<Docker.Container> {
|
|
229
|
+
const containerPromise: Promise<[unknown, Docker.Container]> =
|
|
230
|
+
this.docker.run(
|
|
231
|
+
image,
|
|
232
|
+
command,
|
|
233
|
+
// @ts-expect-error -- The typings expect an array of streams but in reality they're optional
|
|
234
|
+
undefined,
|
|
235
|
+
);
|
|
236
|
+
return Bluebird.resolve(
|
|
237
|
+
containerPromise.then(([, container]) => container),
|
|
238
|
+
).disposer((container) => container.wait().then(() => container.remove()));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
public getDaemonFreeSpace(): Promise<{
|
|
242
|
+
used: number;
|
|
243
|
+
total: number;
|
|
244
|
+
free: number;
|
|
245
|
+
}> {
|
|
246
|
+
return Bluebird.resolve(this.baseImagePromise)
|
|
247
|
+
.tap((baseImage) => {
|
|
248
|
+
// Ensure the image is available (if it is this is essentially a no-op)
|
|
249
|
+
return this.dockerProgress.pull(baseImage, _.noop);
|
|
250
|
+
})
|
|
251
|
+
.then((baseImage) => {
|
|
252
|
+
return this.getOutput(baseImage, ['/bin/df', '-B', '1', '/']);
|
|
253
|
+
})
|
|
254
|
+
.then(function (spaceStr) {
|
|
255
|
+
// First split the lines, as we're only interested in the second one
|
|
256
|
+
const lines = spaceStr.trim().split(/\r?\n/);
|
|
257
|
+
if (lines.length !== 2) {
|
|
258
|
+
throw new Error('Coult not parse df output');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const parts = lines[1].split(/\s+/);
|
|
262
|
+
const total = parseInt(parts[1], 10);
|
|
263
|
+
const used = parseInt(parts[2], 10);
|
|
264
|
+
const free = parseInt(parts[3], 10);
|
|
265
|
+
return { used, total, free };
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private getDaemonArchitecture() {
|
|
270
|
+
return this.docker.version().then(({ Arch }) => Arch);
|
|
271
|
+
}
|
|
272
|
+
}
|
package/package.json
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docker-storage-gc",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"main": "build/index.js",
|
|
5
|
-
"types": "index.d.ts",
|
|
6
5
|
"scripts": {
|
|
7
|
-
"lint": "balena-lint lib test tools",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
6
|
+
"lint": "balena-lint -t tsconfig.dev.json -e ts -e js lib test tools",
|
|
7
|
+
"lint-fix": "balena-lint --fix -t tsconfig.dev.json -e ts -e js lib test tools",
|
|
8
|
+
"pretest": "npm run prepare && docker rm -vf docker-storage-gc-tests && docker run --privileged --name docker-storage-gc-tests -v /tmp/dind:/var/run/ -d docker:24.0.5-dind && sleep 5 && docker exec docker-storage-gc-tests chown $(id -u) /var/run/docker.sock",
|
|
9
|
+
"test": "mocha --exit --require ts-node/register/transpile-only test/**/*.ts",
|
|
10
|
+
"posttest": "docker rm -vf docker-storage-gc-tests && npm run lint",
|
|
11
|
+
"prepare": "npx tsc"
|
|
12
12
|
},
|
|
13
13
|
"author": "",
|
|
14
14
|
"license": "Apache 2.0",
|
|
15
15
|
"description": "Automatically cleanup unused images based on various cache replacement algorithms",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@types/
|
|
17
|
+
"@types/bluebird": "^3.5.42",
|
|
18
|
+
"@types/dockerode": "^3.3.23",
|
|
19
|
+
"@types/event-stream": "^4.0.5",
|
|
20
|
+
"@types/JSONStream": "npm:@types/jsonstream@^0.8.31",
|
|
21
|
+
"@types/lodash": "^4.14.202",
|
|
22
|
+
"@types/node": "^16.18.65",
|
|
18
23
|
"bluebird": "^3.7.2",
|
|
19
24
|
"docker-progress": "^5.2.0",
|
|
20
|
-
"dockerode": "^
|
|
25
|
+
"dockerode": "^3.3.5",
|
|
21
26
|
"event-stream": "^4.0.1",
|
|
22
27
|
"eventemitter3": "^5.0.1",
|
|
23
28
|
"JSONStream": "^1.3.5",
|
|
24
29
|
"lodash": "^4.17.21"
|
|
25
30
|
},
|
|
26
31
|
"devDependencies": {
|
|
27
|
-
"@balena/lint": "^
|
|
32
|
+
"@balena/lint": "^7.2.4",
|
|
33
|
+
"@types/chai": "^4.3.11",
|
|
34
|
+
"@types/mocha": "^10.0.6",
|
|
28
35
|
"chai": "^4.3.10",
|
|
29
|
-
"coffeescript": "^1.12.7",
|
|
30
36
|
"mocha": "^10.2.0",
|
|
31
|
-
"timekeeper": "^2.3.1"
|
|
37
|
+
"timekeeper": "^2.3.1",
|
|
38
|
+
"ts-node": "^10.9.1",
|
|
39
|
+
"typescript": "^5.3.2"
|
|
32
40
|
},
|
|
33
41
|
"versionist": {
|
|
34
|
-
"publishedAt": "2023-12-
|
|
42
|
+
"publishedAt": "2023-12-04T17:24:21.020Z"
|
|
35
43
|
}
|
|
36
44
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { LayerMtimes } from '../build/docker-event-stream';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import es from 'event-stream';
|
|
6
|
+
import { parseEventStream } from '../build/docker-event-stream';
|
|
7
|
+
import { getDocker } from '../build/docker';
|
|
8
|
+
|
|
9
|
+
describe('parseEventStream', function () {
|
|
10
|
+
it.skip('should work with empty stream', function () {
|
|
11
|
+
// TODO
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should return updated mtimes', () =>
|
|
15
|
+
getDocker({})
|
|
16
|
+
.then((docker) => parseEventStream(docker))
|
|
17
|
+
.then(
|
|
18
|
+
(streamParser) =>
|
|
19
|
+
new Promise<LayerMtimes>(function (resolve, reject) {
|
|
20
|
+
let mtimes: LayerMtimes;
|
|
21
|
+
|
|
22
|
+
return fs
|
|
23
|
+
.createReadStream(__dirname + '/fixtures/docker-events.json')
|
|
24
|
+
.pipe(streamParser)
|
|
25
|
+
.on('error', reject)
|
|
26
|
+
.pipe(es.mapSync((data: LayerMtimes) => (mtimes = data)))
|
|
27
|
+
.on('end', () => resolve(mtimes))
|
|
28
|
+
.on('error', reject);
|
|
29
|
+
}),
|
|
30
|
+
)
|
|
31
|
+
.then(function (data) {
|
|
32
|
+
expect(data)
|
|
33
|
+
.to.have.property('busybox:latest')
|
|
34
|
+
.that.equals(1448576072937294800);
|
|
35
|
+
expect(data)
|
|
36
|
+
.to.have.property(
|
|
37
|
+
'sha256:6d41a4a0bf8168363e29da8a5ecbf3cd6c37e3f5a043decd5e7da6e427ba869c',
|
|
38
|
+
)
|
|
39
|
+
.that.equals(1448576073085559800);
|
|
40
|
+
expect(data)
|
|
41
|
+
.to.have.property(
|
|
42
|
+
'sha256:9a61b6b1315e6b457c31a03346ab94486a2f5397f4a82219bee01eead1c34c2e',
|
|
43
|
+
)
|
|
44
|
+
.that.equals(1448576073203895800);
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { ContainerInfo, ImageInfo } from 'dockerode';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import tk from 'timekeeper';
|
|
5
|
+
import es from 'event-stream';
|
|
6
|
+
import { LayerMtimes, parseEventStream } from '../build/docker-event-stream';
|
|
7
|
+
import { createTree } from '../build/docker-image-tree';
|
|
8
|
+
import { getDocker } from '../build/docker';
|
|
9
|
+
|
|
10
|
+
const getLayerMtimes = () =>
|
|
11
|
+
getDocker({})
|
|
12
|
+
.then((docker) => parseEventStream(docker))
|
|
13
|
+
.then(
|
|
14
|
+
(streamParser) =>
|
|
15
|
+
new Promise<LayerMtimes>(function (resolve, reject) {
|
|
16
|
+
let mtimes: LayerMtimes;
|
|
17
|
+
return fs
|
|
18
|
+
.createReadStream(__dirname + '/fixtures/docker-events.json')
|
|
19
|
+
.pipe(streamParser)
|
|
20
|
+
.on('error', reject)
|
|
21
|
+
.pipe(es.mapSync((data: LayerMtimes) => (mtimes = data)))
|
|
22
|
+
.on('end', () => resolve(mtimes))
|
|
23
|
+
.on('error', reject);
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
describe('createTree', function () {
|
|
28
|
+
it.skip('should work with empty input', function () {
|
|
29
|
+
// TODO
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return a tree of images', async function () {
|
|
33
|
+
const images = (
|
|
34
|
+
await import('./fixtures/docker-images.json', {
|
|
35
|
+
assert: { type: 'json' },
|
|
36
|
+
})
|
|
37
|
+
).default as ImageInfo[];
|
|
38
|
+
const containers = (
|
|
39
|
+
await import('./fixtures/docker-containers.json', {
|
|
40
|
+
assert: { type: 'json' },
|
|
41
|
+
})
|
|
42
|
+
).default as ContainerInfo[];
|
|
43
|
+
return getLayerMtimes().then(function (mtimes) {
|
|
44
|
+
tk.freeze(Date.UTC(2016, 0, 1));
|
|
45
|
+
const tree = createTree(images, containers, mtimes);
|
|
46
|
+
tk.reset();
|
|
47
|
+
const output = {
|
|
48
|
+
id: '0000000000000000000000000000000000000000000000000000000000000000',
|
|
49
|
+
size: 0,
|
|
50
|
+
repoTags: [],
|
|
51
|
+
repoDigests: [],
|
|
52
|
+
mtime: 1451606400000000000,
|
|
53
|
+
isUsedByAContainer: false,
|
|
54
|
+
children: {
|
|
55
|
+
'sha256:6d15899cef812e2876b9d5d43d4cd863eda7b278f7b52d00975f6a9a8e817c74':
|
|
56
|
+
{
|
|
57
|
+
id: 'sha256:6d15899cef812e2876b9d5d43d4cd863eda7b278f7b52d00975f6a9a8e817c74',
|
|
58
|
+
size: 125151141,
|
|
59
|
+
repoTags: [],
|
|
60
|
+
repoDigests: [],
|
|
61
|
+
mtime: 1451606400000000000,
|
|
62
|
+
isUsedByAContainer: false,
|
|
63
|
+
children: {
|
|
64
|
+
'sha256:e53bd4df04f86919156c4510cdc6e6c9491ec8ec226381d36aca573b46bbbbbc':
|
|
65
|
+
{
|
|
66
|
+
id: 'sha256:e53bd4df04f86919156c4510cdc6e6c9491ec8ec226381d36aca573b46bbbbbc',
|
|
67
|
+
size: 0,
|
|
68
|
+
repoTags: ['project1'],
|
|
69
|
+
repoDigests: [],
|
|
70
|
+
mtime: 1451606400000000000,
|
|
71
|
+
isUsedByAContainer: false,
|
|
72
|
+
children: {
|
|
73
|
+
'sha256:6d41a4a0bf8168363e29da8a5ecbf3cd6c37e3f5a043decd5e7da6e427ba869c':
|
|
74
|
+
{
|
|
75
|
+
id: 'sha256:6d41a4a0bf8168363e29da8a5ecbf3cd6c37e3f5a043decd5e7da6e427ba869c',
|
|
76
|
+
size: 330389,
|
|
77
|
+
repoTags: ['project2'],
|
|
78
|
+
repoDigests: [],
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
|
|
80
|
+
mtime: 1448576073085559863,
|
|
81
|
+
isUsedByAContainer: false,
|
|
82
|
+
children: {
|
|
83
|
+
'sha256:80dc79d29cd8618e678da508fc32f7289e6f72defb534f3f287731b1f8b355ea':
|
|
84
|
+
{
|
|
85
|
+
id: 'sha256:80dc79d29cd8618e678da508fc32f7289e6f72defb534f3f287731b1f8b355ea',
|
|
86
|
+
size: 98872,
|
|
87
|
+
repoTags: [],
|
|
88
|
+
repoDigests: [],
|
|
89
|
+
mtime: 1451606400000000000,
|
|
90
|
+
isUsedByAContainer: false,
|
|
91
|
+
children: {},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
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: {},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
'sha256:5b0d59026729b68570d99bc4f3f7c31a2e4f2a5736435641565d93e7c25bd2c3':
|
|
121
|
+
{
|
|
122
|
+
id: 'sha256:5b0d59026729b68570d99bc4f3f7c31a2e4f2a5736435641565d93e7c25bd2c3',
|
|
123
|
+
size: 125151141,
|
|
124
|
+
repoTags: ['busybox:latest'],
|
|
125
|
+
repoDigests: [
|
|
126
|
+
'sha256:a8cf7ff6367c2afa2a90acd081b484cbded349a7076e7bdf37a05279f276bc12',
|
|
127
|
+
],
|
|
128
|
+
mtime: 1448576072937294800,
|
|
129
|
+
isUsedByAContainer: true,
|
|
130
|
+
children: {},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
expect(tree).to.deep.equal(output);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|