forked from erikengervall/dockest
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: use docker events for better reliability.
Adds a docker event bus that can be used for improving the dockest experience. It is currently used in the following ways: Wait for container start before trying to resolve the container id (which solves possible timeouts due to first downloading the image) and stop resolving the container id in case the container unexpectedly dies. This results in less flakiness. In the future the readiness check API could be based upon docker events (see erikengervall#85 (comment)).
- Loading branch information
Showing
17 changed files
with
268 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { interval, Subject, race } from 'rxjs' | ||
import { takeUntil, tap, first, mapTo } from 'rxjs/operators' | ||
import { Runner } from '../@types' | ||
|
||
export const createContainerDieCheck = ({ runner }: { runner: Runner }) => { | ||
const { dockerEventStream$ } = runner | ||
const stop$ = new Subject() | ||
const cancel$ = new Subject() | ||
|
||
const info$ = interval(1000).pipe( | ||
takeUntil(stop$), | ||
tap(() => { | ||
runner.logger.info('Container is still running...') | ||
}), | ||
) | ||
|
||
const containerDies$ = dockerEventStream$.pipe( | ||
takeUntil(stop$), | ||
first(event => event.action === 'die'), | ||
) | ||
|
||
return { | ||
service: runner.serviceName, | ||
done: race(containerDies$, info$, cancel$) | ||
.pipe( | ||
tap({ | ||
next: () => { | ||
stop$.next() | ||
stop$.complete() | ||
}, | ||
}), | ||
mapTo(undefined), | ||
) | ||
.toPromise(), | ||
cancel: () => { | ||
cancel$.complete() | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { interval, Subject, race } from 'rxjs' | ||
import { takeUntil, tap, first, mapTo } from 'rxjs/operators' | ||
import { Runner } from '../@types' | ||
|
||
export const createContainerStartCheck = ({ runner }: { runner: Runner }) => { | ||
const { dockerEventStream$ } = runner | ||
const stop$ = new Subject() | ||
|
||
const cancel$ = new Subject() | ||
|
||
const info$ = interval(1000).pipe( | ||
takeUntil(stop$), | ||
tap(() => { | ||
runner.logger.info('Still waiting for start event...') | ||
}), | ||
) | ||
|
||
const containerStarts$ = dockerEventStream$.pipe( | ||
takeUntil(stop$), | ||
first(event => event.action === 'start'), | ||
) | ||
|
||
return { | ||
service: runner.serviceName, | ||
done: race(containerStarts$, info$, cancel$) | ||
.pipe( | ||
tap({ | ||
next: () => { | ||
stop$.next() | ||
stop$.complete() | ||
}, | ||
}), | ||
mapTo(undefined), | ||
) | ||
.toPromise(), | ||
cancel: () => { | ||
cancel$.complete() | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { EventEmitter } from 'events' | ||
import execa from 'execa' /* eslint-disable-line import/default */ | ||
|
||
const parseJsonSafe = (data: string) => { | ||
try { | ||
return JSON.parse(data) | ||
} catch (err) { | ||
return null | ||
} | ||
} | ||
|
||
export interface DockerComposeEventInterface<TActionName extends string, TAdditionalAttributes extends {} = {}> { | ||
time: string | ||
type: 'container' | ||
action: TActionName | ||
id: string | ||
service: string | ||
attributes: { | ||
image: string | ||
name: string | ||
} & TAdditionalAttributes | ||
} | ||
|
||
export type CreateDockerComposeEvent = DockerComposeEventInterface<'create'> | ||
export type AttachDockerComposeEvent = DockerComposeEventInterface<'attach'> | ||
export type StartDockerComposeEvent = DockerComposeEventInterface<'start'> | ||
export type HealthStatusDockerComposeEvent = DockerComposeEventInterface< | ||
'health_status', | ||
{ healthStatus: 'healthy' | 'unhealthy' } | ||
> | ||
export type KillDockerComposeEvent = DockerComposeEventInterface<'kill'> | ||
export type DieDockerComposeEvent = DockerComposeEventInterface<'die'> | ||
|
||
export type DockerEventType = | ||
| CreateDockerComposeEvent | ||
| AttachDockerComposeEvent | ||
| StartDockerComposeEvent | ||
| HealthStatusDockerComposeEvent | ||
| KillDockerComposeEvent | ||
| DieDockerComposeEvent | ||
|
||
export type UnknownDockerComposeEvent = DockerComposeEventInterface<string> | ||
|
||
export type DockerEventEmitterListener = (event: DockerEventType) => void | ||
|
||
export interface DockerEventEmitter { | ||
addListener(serviceName: string, eventListener: DockerEventEmitterListener): void | ||
removeListener(serviceName: string, eventListener: DockerEventEmitterListener): void | ||
destroy(): void | ||
} | ||
|
||
export const createDockerEventEmitter = (composeFilePath: string): DockerEventEmitter => { | ||
const command = ` \ | ||
docker-compose \ | ||
-f ${composeFilePath} \ | ||
events \ | ||
--json | ||
` | ||
const childProcess = execa(command, { shell: true, reject: false }) | ||
|
||
if (!childProcess.stdout) { | ||
childProcess.kill() | ||
throw new Error('Event Process has not output stream.') | ||
} | ||
|
||
const emitter = new EventEmitter() | ||
|
||
// without this line only the first data event is fired (in some undefinable cases) | ||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
childProcess.then(() => {}) | ||
|
||
childProcess.stdout.addListener('data', chunk => { | ||
const lines: string[] = chunk | ||
.toString() | ||
.split(`\n`) | ||
.filter(Boolean) | ||
|
||
for (const line of lines) { | ||
const data: UnknownDockerComposeEvent = parseJsonSafe(line) | ||
if (!data) return | ||
|
||
// convert health status to friendlier format | ||
if (data.action.startsWith('health_status: ')) { | ||
const healthStatus = data.action | ||
.replace('health_status: ', '') | ||
.trim() as HealthStatusDockerComposeEvent['attributes']['healthStatus'] | ||
data.action = 'health_status' | ||
;(data as HealthStatusDockerComposeEvent).attributes.healthStatus = healthStatus | ||
} | ||
|
||
emitter.emit(data.service, data) | ||
} | ||
}) | ||
|
||
return Object.assign(emitter, { | ||
destroy: () => childProcess.cancel(), | ||
}) | ||
} |
23 changes: 23 additions & 0 deletions
23
packages/dockest/src/run/createDockerServiceEventStream.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { fromEventPattern, Observable } from 'rxjs' | ||
import { shareReplay } from 'rxjs/operators' | ||
import { DockerEventEmitter, DockerEventType } from './createDockerEventEmitter' | ||
|
||
export type DockerServiceEventStream = Observable<DockerEventType> | ||
|
||
export const createDockerServiceEventStream = ( | ||
serviceName: string, | ||
eventEmitter: DockerEventEmitter, | ||
): DockerServiceEventStream => { | ||
return ( | ||
fromEventPattern<DockerEventType>( | ||
handler => { | ||
eventEmitter.addListener(serviceName, handler) | ||
}, | ||
handler => { | ||
eventEmitter.removeListener(serviceName, handler) | ||
}, | ||
) | ||
// Every new subscriber should receive access to all previous emitted events, because of this we use shareReplay. | ||
.pipe(shareReplay()) | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.