/
docker-client.ts
168 lines (140 loc) · 5.21 KB
/
docker-client.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import Dockerode, { PortMap as DockerodePortBindings } from "dockerode";
import streamToArray from "stream-to-array";
import tar from "tar-fs";
import { BoundPorts } from "./bound-ports";
import { Container, DockerodeContainer } from "./container";
import { Host } from "./docker-client-factory";
import log from "./logger";
import { PortString } from "./port";
import { RepoTag } from "./repo-tag";
export type Command = string;
export type ContainerName = string;
export type ExitCode = number;
export type EnvKey = string;
export type EnvValue = string;
export type Env = { [key in EnvKey]: EnvValue };
type DockerodeEnvironment = string[];
export type Dir = string;
export type TmpFs = { [dir in Dir]: Dir };
export type BuildContext = string;
export type BuildArgs = { [key in EnvKey]: EnvValue };
export type StreamOutput = string;
export type ExecResult = { output: StreamOutput; exitCode: ExitCode };
type DockerodeExposedPorts = { [port in PortString]: {} };
export type BindMode = "rw" | "ro";
export type BindMount = {
source: Dir;
target: Dir;
bindMode: BindMode;
};
type DockerodeBindMount = string;
type CreateOptions = {
repoTag: RepoTag;
env: Env;
cmd: Command[];
bindMounts: BindMount[];
tmpFs: TmpFs;
boundPorts: BoundPorts;
name?: ContainerName;
};
export interface DockerClient {
pull(repoTag: RepoTag): Promise<void>;
create(options: CreateOptions): Promise<Container>;
start(container: Container): Promise<void>;
exec(container: Container, command: Command[]): Promise<ExecResult>;
buildImage(repoTag: RepoTag, context: BuildContext, buildArgs: BuildArgs): Promise<void>;
fetchRepoTags(): Promise<RepoTag[]>;
getHost(): Host;
}
export class DockerodeClient implements DockerClient {
constructor(private readonly host: Host, private readonly dockerode: Dockerode) {}
public async pull(repoTag: RepoTag): Promise<void> {
log.info(`Pulling image: ${repoTag}`);
const stream = await this.dockerode.pull(repoTag.toString(), {});
await streamToArray(stream);
}
public async create(options: CreateOptions): Promise<Container> {
log.info(`Creating container for image: ${options.repoTag}`);
const dockerodeContainer = await this.dockerode.createContainer({
name: options.name,
Image: options.repoTag.toString(),
Env: this.getEnv(options.env),
ExposedPorts: this.getExposedPorts(options.boundPorts),
Cmd: options.cmd,
HostConfig: {
PortBindings: this.getPortBindings(options.boundPorts),
Binds: this.getBindMounts(options.bindMounts),
Tmpfs: options.tmpFs
}
});
return new DockerodeContainer(dockerodeContainer);
}
public start(container: Container): Promise<void> {
log.info(`Starting container with ID: ${container.getId()}`);
return container.start();
}
public async exec(container: Container, command: Command[]): Promise<ExecResult> {
const exec = await container.exec({
cmd: command,
attachStdout: true,
attachStderr: true
});
const stream = await exec.start();
const output = Buffer.concat(await streamToArray(stream)).toString();
const { exitCode } = await exec.inspect();
return { output, exitCode };
}
public async buildImage(repoTag: RepoTag, context: BuildContext, buildArgs: BuildArgs): Promise<void> {
log.info(`Building image '${repoTag.toString()}' with context '${context}'`);
const tarStream = tar.pack(context);
const stream = await this.dockerode.buildImage(tarStream, {
buildargs: buildArgs,
t: repoTag.toString()
});
await streamToArray(stream);
}
public async fetchRepoTags(): Promise<RepoTag[]> {
const images = await this.dockerode.listImages();
return images.reduce((repoTags: RepoTag[], image) => {
if (this.isDanglingImage(image)) {
return repoTags;
}
const imageRepoTags = image.RepoTags.map(imageRepoTag => {
const [imageName, tag] = imageRepoTag.split(":");
return new RepoTag(imageName, tag);
});
return [...repoTags, ...imageRepoTags];
}, []);
}
public getHost(): Host {
return this.host;
}
private isDanglingImage(image: Dockerode.ImageInfo) {
return image.RepoTags === null;
}
private getEnv(env: Env): DockerodeEnvironment {
return Object.entries(env).reduce(
(dockerodeEnvironment, [key, value]) => {
return [...dockerodeEnvironment, `${key}=${value}`];
},
[] as DockerodeEnvironment
);
}
private getExposedPorts(boundPorts: BoundPorts): DockerodeExposedPorts {
const dockerodeExposedPorts: DockerodeExposedPorts = {};
for (const [internalPort] of boundPorts.iterator()) {
dockerodeExposedPorts[internalPort.toString()] = {};
}
return dockerodeExposedPorts;
}
private getPortBindings(boundPorts: BoundPorts): DockerodePortBindings {
const dockerodePortBindings: DockerodePortBindings = {};
for (const [internalPort, hostPort] of boundPorts.iterator()) {
dockerodePortBindings[internalPort.toString()] = [{ HostPort: hostPort.toString() }];
}
return dockerodePortBindings;
}
private getBindMounts(bindMounts: BindMount[]): DockerodeBindMount[] {
return bindMounts.map(({ source, target, bindMode }) => `${source}:${target}:${bindMode}`);
}
}