-
Notifications
You must be signed in to change notification settings - Fork 267
/
build.ts
261 lines (219 loc) · 8.35 KB
/
build.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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/*
* Copyright (C) 2018-2024 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { containerHelpers } from "./helpers.js"
import { ConfigurationError } from "../../exceptions.js"
import type { PrimitiveMap } from "../../config/common.js"
import split2 from "split2"
import type { BuildActionHandler } from "../../plugin/action-types.js"
import type { ContainerBuildAction, ContainerBuildOutputs } from "./config.js"
import { defaultDockerfileName } from "./config.js"
import { joinWithPosix } from "../../util/fs.js"
import type { Resolved } from "../../actions/types.js"
import dedent from "dedent"
import {
CONTAINER_BUILD_CONCURRENCY_LIMIT_CLOUD_BUILDER,
CONTAINER_BUILD_CONCURRENCY_LIMIT_LOCAL,
CONTAINER_STATUS_CONCURRENCY_LIMIT,
type ContainerProviderConfig,
} from "./container.js"
import type { Writable } from "stream"
import type { ActionLog } from "../../logger/log-entry.js"
import type { PluginContext } from "../../plugin-context.js"
import type { SpawnOutput } from "../../util/util.js"
import { cloudBuilder } from "./cloudbuilder.js"
import { styles } from "../../logger/styles.js"
export const validateContainerBuild: BuildActionHandler<"validate", ContainerBuildAction> = async ({ action }) => {
// configure concurrency limit for build status task nodes.
action.statusConcurrencyLimit = CONTAINER_STATUS_CONCURRENCY_LIMIT
return {}
}
export const getContainerBuildStatus: BuildActionHandler<"getStatus", ContainerBuildAction> = async ({
ctx,
action,
log,
}) => {
// configure concurrency limit for build execute task nodes.
if (await cloudBuilder.isConfiguredAndAvailable(ctx, action)) {
action.executeConcurrencyLimit = CONTAINER_BUILD_CONCURRENCY_LIMIT_CLOUD_BUILDER
} else {
action.executeConcurrencyLimit = CONTAINER_BUILD_CONCURRENCY_LIMIT_LOCAL
}
const outputs = action.getOutputs()
const { identifier } = (await containerHelpers.getLocalImageInfo(outputs.localImageId, log, ctx)) || {}
if (identifier) {
log.debug(`Image ${identifier} already exists`)
}
const state = !!identifier ? "ready" : "not-ready"
return { state, detail: {}, outputs }
}
export const buildContainer: BuildActionHandler<"build", ContainerBuildAction> = async ({ ctx, action, log }) => {
containerHelpers.checkDockerServerVersion(await containerHelpers.getDockerVersion(), log)
const outputs = action.getOutputs()
const identifier = outputs.localImageId
const hasDockerfile = await containerHelpers.actionHasDockerfile(action)
// make sure we can build the thing
if (!hasDockerfile) {
throw new ConfigurationError({
message: dedent`
Dockerfile not found at ${action.getSpec().dockerfile || defaultDockerfileName} for build ${action.name}.
Please make sure the file exists, and is not excluded by include/exclude fields or .gardenignore files.
`,
})
}
const logEventContext = {
origin: "docker build",
level: "verbose" as const,
}
const outputStream = split2()
outputStream.on("error", () => {})
outputStream.on("data", (line: Buffer) => {
ctx.events.emit("log", { timestamp: new Date().toISOString(), msg: line.toString(), ...logEventContext })
})
const timeout = action.getConfig("timeout")
let res: SpawnOutput
if (await cloudBuilder.isConfiguredAndAvailable(ctx, action)) {
res = await buildContainerInCloudBuilder({ action, outputStream, timeout, log, ctx })
} else {
res = await buildContainerLocally({
action,
outputStream,
timeout,
log,
ctx,
})
}
return {
state: "ready",
outputs,
detail: { fresh: true, buildLog: res.all || "", outputs, details: { identifier } },
}
}
async function buildContainerLocally({
action,
outputStream,
timeout,
log,
ctx,
extraDockerOpts = [],
}: {
action: Resolved<ContainerBuildAction>
outputStream: Writable
timeout: number
log: ActionLog
ctx: PluginContext<ContainerProviderConfig>
extraDockerOpts?: string[]
}) {
const spec = action.getSpec()
const outputs = action.getOutputs()
const buildPath = action.getBuildPath()
log.info(`Building ${outputs.localImageId}...`)
const dockerfilePath = joinWithPosix(buildPath, spec.dockerfile)
const dockerFlags = [...getDockerBuildFlags(action, ctx.provider.config), ...extraDockerOpts]
// If there already is a --tag flag, another plugin like the Kubernetes plugin already decided how to tag the image.
// In this case, we don't want to add another local tag.
// TODO: it would be nice to find a better way to become aware of the parent plugin's concerns in the container plugin.
if (!dockerFlags.includes("--tag")) {
dockerFlags.push(...["--tag", outputs.localImageId])
// if deploymentImageId is different from localImageId, tag the image with deploymentImageId as well.
if (outputs.deploymentImageId && outputs.localImageId !== outputs.deploymentImageId) {
dockerFlags.push(...["--tag", outputs.deploymentImageId])
}
}
const cmdOpts = ["build", ...dockerFlags, "--file", dockerfilePath]
return await containerHelpers.dockerCli({
cwd: buildPath,
args: [...cmdOpts, buildPath],
log,
stdout: outputStream,
stderr: outputStream,
timeout,
ctx,
})
}
const BUILDKIT_LAYER_REGEX = /^#[0-9]+ \[[^ ]+ +[0-9]+\/[0-9]+\] [^F][^R][^O][^M]/
const BUILDKIT_LAYER_CACHED_REGEX = /^#[0-9]+ CACHED/
async function buildContainerInCloudBuilder(params: {
action: Resolved<ContainerBuildAction>
outputStream: Writable
timeout: number
log: ActionLog
ctx: PluginContext<ContainerProviderConfig>
}) {
const cloudbuilderStats = {
totalLayers: 0,
layersCached: 0,
}
// get basic buildkit stats
params.outputStream.on("data", (line: Buffer) => {
const logLine = line.toString()
if (BUILDKIT_LAYER_REGEX.test(logLine)) {
cloudbuilderStats.totalLayers++
} else if (BUILDKIT_LAYER_CACHED_REGEX.test(logLine)) {
cloudbuilderStats.layersCached++
}
})
const res = await cloudBuilder.withBuilder(params.ctx, params.action, async (builderName) => {
const extraDockerOpts = ["--builder", builderName]
// we add --push in the Kubernetes local-docker handler when using the Kubernetes plugin with a deploymentRegistry setting.
// If we have --push, no need to --load.
if (!getDockerBuildFlags(params.action, params.ctx.provider.config).includes("--push")) {
// This action makes sure to download the image from the cloud builder, and make it available locally.
extraDockerOpts.push("--load")
}
return await buildContainerLocally({ ...params, extraDockerOpts })
})
const log = params.ctx.log.createLog({
name: `build.${params.action.name}`,
})
log.success(
`${styles.bold("Accelerated by Garden Cloud Builder")} (${cloudbuilderStats.layersCached}/${cloudbuilderStats.totalLayers} layers cached)`
)
return res
}
export function getContainerBuildActionOutputs(action: Resolved<ContainerBuildAction>): ContainerBuildOutputs {
return containerHelpers.getBuildActionOutputs(action, undefined)
}
export function getDockerBuildFlags(
action: Resolved<ContainerBuildAction>,
containerProviderConfig: ContainerProviderConfig
) {
const args: string[] = []
const { targetStage, extraFlags, buildArgs } = action.getSpec()
for (const arg of getDockerBuildArgs(action.versionString(), buildArgs)) {
args.push("--build-arg", arg)
}
if (targetStage) {
args.push("--target", targetStage)
}
args.push(...(extraFlags || []))
args.push(...(containerProviderConfig.dockerBuildExtraFlags || []))
return args
}
export function getDockerBuildArgs(version: string, specBuildArgs: PrimitiveMap) {
const buildArgs: PrimitiveMap = {
GARDEN_MODULE_VERSION: version,
GARDEN_ACTION_VERSION: version,
...specBuildArgs,
}
return Object.entries(buildArgs)
.map(([key, value]) => {
// If the value is empty, we simply don't pass it to docker
if (value === "") {
return undefined
}
// 0 is falsy
if (value || value === 0) {
return `${key}=${value}`
} else {
// If the value of a build-arg is null, Docker pulls it from
// the environment: https://docs.docker.com/engine/reference/commandline/build/
return key
}
})
.filter((x): x is string => !!x)
}