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/test/index.ts ADDED
@@ -0,0 +1,228 @@
1
+ import type Docker from 'dockerode';
2
+ import Bluebird from 'bluebird';
3
+ import { expect } from 'chai';
4
+ import DockerGC from '../build/index';
5
+ import { getDocker } from '../build/docker';
6
+
7
+ const SKIP_GC_TEST = process.env.SKIP_GC_TEST === '1' || false;
8
+ const IMAGES = ['alpine:3.1', 'debian:squeeze', 'ubuntu:lucid'];
9
+
10
+ // TODO: Move it to a proper repo
11
+ // Same image (same id), different repo, different digest
12
+ const NONE_TAG_IMAGES = [
13
+ 'hello-world@sha256:8e3114318a995a1ee497790535e7b88365222a21771ae7e53687ad76563e8e76',
14
+ 'balenaplayground/hello-world@sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042',
15
+ ];
16
+
17
+ const promiseToBool = (p: Promise<unknown>): Promise<boolean> =>
18
+ p.then(() => true).catch(() => false);
19
+
20
+ const pullAsync = function (docker: Docker, tag: string) {
21
+ console.log(`[TEST] Pulling ${tag}`);
22
+ return docker.pull(tag).then(
23
+ (stream) =>
24
+ new Promise(function (resolve, reject) {
25
+ stream.resume();
26
+ stream.once('error', reject);
27
+ stream.once('end', resolve);
28
+ }),
29
+ );
30
+ };
31
+
32
+ // This test case is a little weird, it requires that no other images are present on
33
+ // the system to ensure that the correct one is being removed. Because of this, you
34
+ // can use the SKIP_GC_TEST env var to inform the test suite not to run this test
35
+ describe('Garbage collection', function () {
36
+ let dockerStorage: DockerGC;
37
+ let docker: Docker;
38
+ beforeEach(function () {
39
+ dockerStorage = new DockerGC();
40
+ // Use either local or CI docker
41
+ return Bluebird.join(
42
+ getDocker({
43
+ socketPath: '/tmp/dind/docker.sock',
44
+ }),
45
+ dockerStorage.setDocker({
46
+ socketPath: '/tmp/dind/docker.sock',
47
+ }),
48
+ ($docker) => {
49
+ docker = $docker;
50
+ return dockerStorage.setupMtimeStream();
51
+ },
52
+ );
53
+ });
54
+
55
+ afterEach(function () {
56
+ console.log('[afterEach] Cleaning up...');
57
+ return IMAGES.concat(NONE_TAG_IMAGES).map((image) =>
58
+ docker
59
+ .getImage(image)
60
+ .remove()
61
+ .catch(function () {
62
+ // Ignore
63
+ }),
64
+ );
65
+ });
66
+
67
+ it('should remove a image by tag', function () {
68
+ this.timeout(600000);
69
+
70
+ return pullAsync(docker, IMAGES[0])
71
+ .then(() => docker.getImage(IMAGES[0]).inspect())
72
+ .then(() => dockerStorage.garbageCollect(1))
73
+ .then(() => promiseToBool(docker.getImage(IMAGES[0]).inspect()))
74
+ .then((imageFound) => expect(imageFound).to.be.false);
75
+ });
76
+
77
+ it('should remove a image by digest if its tag == none', function () {
78
+ this.timeout(600000);
79
+
80
+ return pullAsync(docker, NONE_TAG_IMAGES[0])
81
+ .then(() => docker.getImage(NONE_TAG_IMAGES[0]).inspect())
82
+ .then(() => dockerStorage.garbageCollect(1))
83
+ .then(() => promiseToBool(docker.getImage(NONE_TAG_IMAGES[0]).inspect()))
84
+ .then((imageFound) => expect(imageFound).to.be.false);
85
+ });
86
+
87
+ it('should remove a image with tag == none even if it is in several repos', function () {
88
+ this.timeout(600000);
89
+
90
+ return Bluebird.each(NONE_TAG_IMAGES, (image) => pullAsync(docker, image))
91
+ .then(() => docker.getImage(NONE_TAG_IMAGES[0]).inspect())
92
+ .then(() => dockerStorage.garbageCollect(1))
93
+ .then(() =>
94
+ Bluebird.map(NONE_TAG_IMAGES, (image) =>
95
+ promiseToBool(docker.getImage(image).inspect()),
96
+ ),
97
+ )
98
+ .then((imagesFound) => expect(imagesFound).to.deep.equal([false, false]));
99
+ });
100
+
101
+ it('should remove all tags of an image', function () {
102
+ this.timeout(600000);
103
+ if (SKIP_GC_TEST) {
104
+ return Promise.resolve();
105
+ }
106
+
107
+ // first pull some images, so we know in which order they are referenced
108
+ return pullAsync(docker, IMAGES[0])
109
+ .then(() =>
110
+ docker.getImage(IMAGES[0]).tag({ repo: 'some-repo', tag: 'some-tag' }),
111
+ )
112
+ .then(() => {
113
+ return dockerStorage.garbageCollect(1);
114
+ })
115
+ .then(() => promiseToBool(docker.getImage(IMAGES[0]).inspect()))
116
+ .then((imagesFound) => expect(imagesFound).to.be.false);
117
+ });
118
+
119
+ it('should remove the LRU image', function () {
120
+ this.timeout(600000);
121
+ if (SKIP_GC_TEST) {
122
+ return Promise.resolve();
123
+ }
124
+
125
+ // first pull some images, so we know in which order they are referenced
126
+ return pullAsync(docker, IMAGES[0])
127
+ .then(() =>
128
+ docker.getImage(IMAGES[0]).tag({ repo: 'some-repo', tag: 'some-tag' }),
129
+ )
130
+ .then(() =>
131
+ Bluebird.each(IMAGES.slice(1), (image) => pullAsync(docker, image)),
132
+ )
133
+ .then(() => {
134
+ // Attempt to remove a single byte, which will remove the LRU image,
135
+ // which should be alpine
136
+ return dockerStorage.garbageCollect(1);
137
+ })
138
+ .then(() =>
139
+ Bluebird.map(IMAGES, (image) =>
140
+ promiseToBool(docker.getImage(image).inspect()),
141
+ ),
142
+ )
143
+ .then((imagesFound) =>
144
+ expect(imagesFound).to.deep.equal([false, true, true]),
145
+ );
146
+ });
147
+
148
+ it('should remove more than one image if necessary', function () {
149
+ this.timeout(600000);
150
+ if (SKIP_GC_TEST) {
151
+ return Promise.resolve();
152
+ }
153
+
154
+ return Bluebird.each(IMAGES, (image) => pullAsync(docker, image))
155
+ .then(() =>
156
+ // Get the size of the first image, so we can add one to it to remove
157
+ // the next one in addition
158
+ docker
159
+ .getImage(IMAGES[0])
160
+ .inspect()
161
+ .then((i) => i.Size),
162
+ )
163
+ .then((size) => {
164
+ return dockerStorage.garbageCollect(size + 1);
165
+ })
166
+ .then(() =>
167
+ Bluebird.map(IMAGES, (image) =>
168
+ promiseToBool(docker.getImage(image).inspect()),
169
+ ),
170
+ )
171
+ .then((imagesFound) =>
172
+ expect(imagesFound).to.deep.equal([false, false, true]),
173
+ );
174
+ });
175
+
176
+ it('should not consider images in use', function () {
177
+ this.timeout(600000);
178
+ const containerName = 'dont-consider-images-in-use-test';
179
+ if (SKIP_GC_TEST) {
180
+ return Promise.resolve();
181
+ }
182
+
183
+ return pullAsync(docker, IMAGES[0])
184
+ .then(() =>
185
+ docker.createContainer({
186
+ Image: IMAGES[0],
187
+ Tty: true,
188
+ Cmd: ['sh', '-c', 'while true; do echo test; sleep 1; done'],
189
+ name: containerName,
190
+ HostConfig: { AutoRemove: true },
191
+ }),
192
+ )
193
+ .then((container) => container.start())
194
+ .then(() => {
195
+ return dockerStorage.garbageCollect(1);
196
+ })
197
+ .then(() => promiseToBool(docker.getImage(IMAGES[0]).inspect()))
198
+ .then((imageInspect) => expect(imageInspect).to.be.true)
199
+ .finally(() => docker.getContainer(containerName).stop());
200
+ });
201
+
202
+ it('should get daemon host disk usage', function () {
203
+ this.timeout(600000);
204
+ return dockerStorage.getDaemonFreeSpace().then(function (du) {
205
+ expect(du).to.be.an('object');
206
+ expect(du).to.have.property('free').that.is.a('number');
207
+ expect(du).to.have.property('used').that.is.a('number');
208
+ expect(du).to.have.property('total').that.is.a('number');
209
+ });
210
+ });
211
+
212
+ it('should get the correct architecture for a remote host', function () {
213
+ return (
214
+ (
215
+ dockerStorage
216
+ // @ts-expect-error getDaemonArchitecture is private
217
+ .getDaemonArchitecture() as Promise<string>
218
+ ).then((arch) => expect(arch).to.be.a('string'))
219
+ );
220
+ });
221
+
222
+ it('should set a base image to be used', function () {
223
+ // @ts-expect-error baseImagePromise is private
224
+ return (dockerStorage.baseImagePromise as Promise<string>).then((img) =>
225
+ expect(img).to.be.a('string'),
226
+ );
227
+ });
228
+ });
@@ -0,0 +1,39 @@
1
+ import type { ImageNode } from '../lib/docker-image-tree';
2
+
3
+ const id = (tree: ImageNode) => `foo_${tree.id.slice(0, 7)}`;
4
+
5
+ const label = function (tree: ImageNode) {
6
+ const name = tree.repoTags[0] || '\\<none\\>:\\<none\\>';
7
+ const mtime = new Date(tree.mtime!).toISOString();
8
+
9
+ return `label="{ ${tree.id.slice(0, 13)} | ${name} | { ${mtime} | ${
10
+ tree.size
11
+ } } }"`;
12
+ };
13
+
14
+ const color = function (tree: ImageNode) {
15
+ if (tree.repoTags.length === 0) {
16
+ return 'color="black"';
17
+ } else {
18
+ return 'color="red"';
19
+ }
20
+ };
21
+
22
+ const $createDot = function (tree: ImageNode): string[] {
23
+ // define the node
24
+ return (
25
+ [`${id(tree)} [shape=record, ${label(tree)}, ${color(tree)}]`]
26
+ // define all relations with child nodes
27
+ .concat(
28
+ Object.values(tree.children).map(
29
+ (child) => `${id(tree)} -> ${id(child)}`,
30
+ ),
31
+ )
32
+ // recurse to children
33
+ .concat(...Object.values(tree.children).map((child) => $createDot(child)))
34
+ );
35
+ };
36
+
37
+ export function createDot(tree: ImageNode) {
38
+ return 'digraph {\n' + $createDot(tree).join('\n') + '\n}\n';
39
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "allowJs": true,
6
+ "checkJs": false
7
+ },
8
+ "include": [
9
+ "lib/**/*",
10
+ "test/**/*",
11
+ "tools/**/*"
12
+ ]
13
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "Node16",
4
+ "strict": true,
5
+ "strictPropertyInitialization": false,
6
+ "noUnusedParameters": true,
7
+ "noUnusedLocals": true,
8
+ "outDir": "build/",
9
+ "preserveConstEnums": true,
10
+ "removeComments": true,
11
+ "rootDir": "lib",
12
+ "sourceMap": true,
13
+ "target": "es2022",
14
+ "declaration": true,
15
+ "skipLibCheck": true,
16
+ "resolveJsonModule": true,
17
+ "esModuleInterop": true
18
+ },
19
+ "include": [
20
+ "lib/**/*"
21
+ ]
22
+ }
package/index.d.ts DELETED
@@ -1,23 +0,0 @@
1
- import type * as Dockerode from 'dockerode';
2
- import type EventEmitter from 'eventemitter3';
3
-
4
- interface Events {
5
- numberImagesToRemove(n: number): void;
6
- gcRunTime(duration: number): void;
7
- imageRemoved(removalType: string): void;
8
- spaceReclaimed(reclaimSpace: number): void;
9
- imageRemovalError(statusCode: string): void;
10
- }
11
- declare class DockerGC {
12
- setHostname(hostname: string): void;
13
- setupMtimeStream(): Promise<void>;
14
- setDocker(dockerOpts: Dockerode.DockerOptions): Promise<void>;
15
- garbageCollect(reclaimSpace: number, attemptAll?: boolean): Promise<void>;
16
- getDaemonFreeSpace(): Promise<{
17
- used: number;
18
- total: number;
19
- free: number;
20
- }>;
21
- metrics: EventEmitter<Events>;
22
- }
23
- export = DockerGC;
@@ -1,46 +0,0 @@
1
- Bluebird = require 'bluebird'
2
- es = require 'event-stream'
3
- JSONStream = require 'JSONStream'
4
-
5
- IMAGE_EVENTS = [ 'delete', 'import', 'pull', 'push', 'tag' ]
6
-
7
- CONTAINER_EVENTS = [
8
- 'attach', 'commit', 'copy', 'create', 'destroy', 'die', 'exec_create',
9
- 'exec_start', 'export', 'kill', 'oom', 'pause', 'rename', 'resize',
10
- 'restart', 'start', 'stop', 'top', 'unpause'
11
- ]
12
-
13
- exports.parseEventStream = parseEventStream = (docker) ->
14
- docker.listImages(all: true).then((images) ->
15
- layer_mtimes = {}
16
- # Start off by setting all current images to an mtime of 0 as we've never seen them used
17
- # If we've never seen the layer used then it's likely created before we started
18
- # listening and so set the last used time to 0 as we know it should be older than
19
- # anything we've seen
20
- for image in images
21
- layer_mtimes[image.Id] = 0
22
-
23
- return es.pipeline(
24
- JSONStream.parse()
25
- es.mapSync ({ status, id, from, timeNano }) ->
26
- if status in IMAGE_EVENTS
27
- if status == 'delete'
28
- if layer_mtimes[id]? then delete layer_mtimes[id]
29
- else
30
- layer_mtimes[id] = timeNano
31
- else if status in CONTAINER_EVENTS
32
- layer_mtimes[from] = timeNano
33
- return layer_mtimes
34
- )
35
- )
36
-
37
- exports.dockerMtimeStream = (docker) ->
38
- Bluebird.join(
39
- docker.getEvents()
40
- parseEventStream(docker)
41
- (stream, streamParser) ->
42
- es.pipeline(
43
- stream
44
- streamParser
45
- )
46
- )
@@ -1,56 +0,0 @@
1
- _ = require 'lodash'
2
- Bluebird = require 'bluebird'
3
-
4
- saneRepoAttrs = (repoAttrs) ->
5
- return [] if !repoAttrs?
6
- return if '<none>:<none>' in repoAttrs or '<none>@<none>' in repoAttrs then [] else repoAttrs
7
-
8
- exports.createNode = createNode = (id) -> { id: id, size: 0, repoTags: [], repoDigests: [], mtime: null, children: {} }
9
-
10
- getMtimeFrom = (layer_mtimes, attributes) ->
11
- for key in attributes
12
- if layer_mtimes[key]?
13
- return layer_mtimes[key]
14
-
15
- getMtime = (tree, layer_mtimes) ->
16
- mtime = layer_mtimes[tree.id]
17
- if mtime == undefined
18
- mtime = getMtimeFrom(layer_mtimes, tree.repoTags)
19
- if mtime == undefined
20
- mtime = getMtimeFrom(layer_mtimes, tree.repoDigests)
21
- return mtime
22
-
23
- exports.createTree = createTree = (images, containers, layer_mtimes) ->
24
- now = Date.now() * 10 ** 6 # convert to nanoseconds
25
- usedImageIds = new Set(
26
- _(containers)
27
- .map('ImageID')
28
- )
29
- tree = {}
30
- root = '0000000000000000000000000000000000000000000000000000000000000000'
31
-
32
- for image in images
33
- node = tree[image.Id] ?= createNode(image.Id)
34
- parentId = image.ParentId or root
35
- parent = tree[parentId] ?= createNode(parentId)
36
-
37
- node.repoTags = saneRepoAttrs(image.RepoTags)
38
- node.repoDigests = saneRepoAttrs(image.RepoDigests)
39
- node.size = image.Size
40
- # If we haven't seen the image at all then assume it is brand new and default it's
41
- # mtime to `now` to avoid removing it
42
- node.mtime = getMtime(node, layer_mtimes) ? now
43
- node.isUsedByAContainer = usedImageIds.has(image.Id)
44
- parent.children[image.Id] = node
45
-
46
- tree[root].mtime = now
47
- tree[root].isUsedByAContainer = false
48
- return tree[root]
49
-
50
- exports.dockerImageTree = (docker, layer_mtimes) ->
51
- Bluebird.join(
52
- docker.listImages(all: true)
53
- docker.listContainers(all: true)
54
- layer_mtimes
55
- createTree
56
- )
package/lib/docker.coffee DELETED
@@ -1,13 +0,0 @@
1
- Docker = require 'dockerode'
2
- Bluebird = require 'bluebird'
3
- _ = require 'lodash'
4
-
5
- getDockerConnectOpts = (hostObj) ->
6
- if !_.isEmpty(hostObj)
7
- return Bluebird.resolve(hostObj)
8
- return Bluebird.resolve({ socketPath: '/var/run/docker.sock', Promise: Bluebird })
9
-
10
- exports.getDocker = (hostObj) ->
11
- getDockerConnectOpts(hostObj)
12
- .then (opts) ->
13
- return new Docker(opts)
package/lib/index.coffee DELETED
@@ -1,168 +0,0 @@
1
- Bluebird = require 'bluebird'
2
- _ = require 'lodash'
3
- { EventEmitter } = require 'eventemitter3'
4
-
5
- { DockerProgress } = require 'docker-progress'
6
- Docker = require 'dockerode'
7
-
8
- { dockerMtimeStream } = require './docker-event-stream'
9
- { dockerImageTree } = require './docker-image-tree'
10
- dockerUtils = require './docker'
11
-
12
- getUnusedTreeLeafs = (tree, result = []) ->
13
- if not tree.removed
14
- children = _(tree.children)
15
- .values()
16
- .filter(_.negate(_.property('removed')))
17
- .value()
18
- if children.length == 0 and not tree.isUsedByAContainer
19
- result.push(tree)
20
- else
21
- for child in children
22
- getUnusedTreeLeafs(child, result)
23
- return result
24
-
25
- getImagesToRemove = (tree, reclaimSpace, metrics) ->
26
- # Removes the oldest, largest leafs first.
27
- # This should avoid trying to remove images with children.
28
- tree = _.clone(tree)
29
- result = []
30
- size = 0
31
- while size < reclaimSpace
32
- leafs = _.orderBy(
33
- getUnusedTreeLeafs(tree)
34
- [ 'mtime', 'size' ]
35
- [ 'asc', 'desc' ]
36
- )
37
- if leafs.length == 0
38
- break
39
- leaf = leafs[0]
40
- if leaf != tree
41
- # don't remove the tree root
42
- result.push(leaf)
43
- size += leaf.size
44
- leaf.removed = true
45
-
46
- metrics.emit('numberImagesToRemove', result.length)
47
- return result
48
-
49
- streamToString = (stream) ->
50
- new Bluebird (resolve, reject) ->
51
- chunks = []
52
- stream
53
- .on('error', reject)
54
- .on 'data', (chunk) ->
55
- chunks.push(chunk)
56
- .on 'end', ->
57
- resolve(Buffer.concat(chunks).toString())
58
-
59
- recordGcRunTime = (t0, metrics) ->
60
- dt = process.hrtime(t0)
61
- duration = dt[0] * 1000 + dt[1] / 1e6
62
- metrics.emit('gcRunTime', duration)
63
-
64
- class DockerGC
65
-
66
- constructor: ->
67
- @metrics = new EventEmitter()
68
- @host = 'unknown'
69
-
70
- setHostname: (hostname) ->
71
- @host = hostname
72
-
73
- setDocker: (hostObj) ->
74
- @currentMtimes = {}
75
- @hostObj = _.defaults({ Promise: Bluebird }, hostObj)
76
- @dockerProgress = new DockerProgress({ docker: new Docker(@hostObj) })
77
- dockerUtils.getDocker(@hostObj)
78
- .then (@docker) =>
79
- # Docker info can take a while so do it here,
80
- # and don't wait on the results
81
- @baseImagePromise = @getDaemonArchitecture()
82
- .then (arch) ->
83
- return switch arch
84
- when 'arm' then 'arm32v6/alpine:3.6'
85
- when 'arm64' then 'arm64v8/alpine:3.6'
86
- when 'amd64' then 'alpine:3.6'
87
- else
88
- throw new Error('Could not detect architecture of remote host')
89
-
90
- setupMtimeStream: ->
91
- dockerMtimeStream(@docker)
92
- .then (stream) =>
93
- stream
94
- .on 'data', (layer_mtimes) =>
95
- @currentMtimes = layer_mtimes
96
-
97
- removeImage: (image) =>
98
- return this.tryRemoveImageBy(image, image.repoTags, 'tag') ||
99
- this.tryRemoveImageBy(image, image.repoDigests, 'digest') ||
100
- this.tryRemoveImageBy(image, [image.id], 'id')
101
-
102
- tryRemoveImageBy: (image, attributes, removalType) =>
103
- if attributes? and attributes.length > 0
104
- Bluebird.each attributes, (attribute) =>
105
- console.log("[GC (#{@host}] Removing image : #{attribute} (id: #{image.id})")
106
- @docker.getImage(attribute).remove(noprune: true)
107
- .then =>
108
- @metrics.emit('imageRemoved', removalType)
109
-
110
- garbageCollect: (reclaimSpace, attemptAll = false) =>
111
- err = null
112
- startTime = process.hrtime()
113
- @metrics.emit('spaceReclaimed', reclaimSpace)
114
- dockerImageTree(@docker, @currentMtimes)
115
- .then (tree) =>
116
- getImagesToRemove(tree, reclaimSpace, @metrics)
117
- .each (image) =>
118
- @removeImage(image)
119
- .catch (e) =>
120
- @metrics.emit('imageRemovalError', e.statusCode)
121
- console.log("[GC #{@host}]: Failed to remove image: ", image)
122
- console.log(e)
123
- if attemptAll
124
- err ?= e
125
- else
126
- recordGcRunTime(startTime, @metrics)
127
- throw e
128
- .then =>
129
- recordGcRunTime(startTime, @metrics)
130
- if err?
131
- throw err
132
-
133
- getOutput: (image, command) ->
134
- Bluebird.using @runDisposer(image, command), (container) ->
135
- container.logs(stdout: true, follow: true)
136
- .then (logs) ->
137
- streamToString(logs)
138
-
139
- runDisposer: (image, command) ->
140
- @docker.run(image, command)
141
- .disposer (container) ->
142
- container.wait()
143
- .then ->
144
- container.remove()
145
-
146
- getDaemonFreeSpace: ->
147
- @baseImagePromise.tap (baseImage) =>
148
- # Ensure the image is available (if it is this is essentially a no-op)
149
- @dockerProgress.pull(baseImage, _.noop)
150
- .then (baseImage) =>
151
- @getOutput(baseImage, [ '/bin/df', '-B', '1', '/' ])
152
- .then (spaceStr) ->
153
- # First split the lines, as we're only interested in the second one
154
- lines = spaceStr.trim().split(/\r?\n/)
155
- if lines.length isnt 2
156
- throw new Error('Coult not parse df output')
157
-
158
- parts = lines[1].split(/\s+/)
159
- total = parseInt(parts[1])
160
- used = parseInt(parts[2])
161
- free = parseInt(parts[3])
162
- return { used, total, free }
163
-
164
- getDaemonArchitecture: ->
165
- @docker.version()
166
- .get('Arch')
167
-
168
- module.exports = DockerGC
@@ -1,30 +0,0 @@
1
- { expect } = require 'chai'
2
- fs = require 'fs'
3
- es = require 'event-stream'
4
-
5
- { parseEventStream } = require '../lib/docker-event-stream'
6
-
7
- dockerUtils = require('../lib/docker.coffee')
8
-
9
- describe 'parseEventStream', ->
10
- it.skip 'should work with empty stream', ->
11
-
12
- it 'should return updated mtimes', ->
13
- dockerUtils.getDocker({})
14
- .then (docker) ->
15
- parseEventStream(docker)
16
- .then (streamParser) ->
17
- new Promise (resolve, reject) ->
18
- mtimes = null
19
-
20
- fs.createReadStream(__dirname + '/fixtures/docker-events.json')
21
- .pipe(streamParser)
22
- .on 'error', reject
23
- .pipe es.mapSync (data) ->
24
- mtimes = data
25
- .on 'end', -> resolve(mtimes)
26
- .on 'error', reject
27
- .then (data) ->
28
- expect(data).to.have.property('busybox:latest').that.equals(1448576072937294800)
29
- expect(data).to.have.property('sha256:6d41a4a0bf8168363e29da8a5ecbf3cd6c37e3f5a043decd5e7da6e427ba869c').that.equals(1448576073085559800)
30
- expect(data).to.have.property('sha256:9a61b6b1315e6b457c31a03346ab94486a2f5397f4a82219bee01eead1c34c2e').that.equals(1448576073203895800)