Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/image-builder-bob/pkg/builder/builder.go
2499 views
1
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package builder
6
7
import (
8
"bytes"
9
"context"
10
"fmt"
11
"io/ioutil"
12
"os"
13
"os/exec"
14
"path/filepath"
15
"runtime"
16
"syscall"
17
"time"
18
19
"github.com/gitpod-io/gitpod/common-go/log"
20
21
"github.com/docker/cli/cli/config/configfile"
22
"github.com/docker/cli/cli/config/types"
23
"github.com/google/go-containerregistry/pkg/crane"
24
"github.com/google/go-containerregistry/pkg/logs"
25
"github.com/moby/buildkit/client"
26
"golang.org/x/xerrors"
27
)
28
29
const (
30
buildkitdSocketPath = "unix:///run/buildkit/buildkitd.sock"
31
// maxConnectionAttempts is the number of attempts to try to connect to the buildkit daemon.
32
// Uses exponential backoff to retry. 8 attempts is a bit over 4 minutes.
33
maxConnectionAttempts = 8
34
initialConnectionTimeout = 2 * time.Second
35
)
36
37
// Builder builds images using buildkit
38
type Builder struct {
39
Config *Config
40
}
41
42
// Build runs the actual image build
43
func (b *Builder) Build() error {
44
var (
45
cl *client.Client
46
teardown func() error = func() error { return nil }
47
err error
48
)
49
if b.Config.ExternalBuildkitd != "" {
50
log.WithField("socketPath", b.Config.ExternalBuildkitd).Info("using external buildkit daemon")
51
cl, err = connectToBuildkitd(b.Config.ExternalBuildkitd)
52
53
if err != nil {
54
log.Warn("cannot connect to node-local buildkitd - falling back to pod-local one")
55
cl, teardown, err = StartBuildkit(buildkitdSocketPath)
56
}
57
} else {
58
cl, teardown, err = StartBuildkit(buildkitdSocketPath)
59
}
60
if err != nil {
61
return err
62
}
63
defer teardown()
64
65
ctx, cancel := context.WithCancel(context.Background())
66
defer cancel()
67
68
err = b.buildBaseLayer(ctx, cl)
69
if err != nil {
70
return err
71
}
72
err = b.buildWorkspaceImage(ctx)
73
if err != nil {
74
return err
75
}
76
77
return nil
78
}
79
80
func (b *Builder) buildBaseLayer(ctx context.Context, cl *client.Client) error {
81
if !b.Config.BuildBase {
82
return nil
83
}
84
85
log.Info("building base image")
86
return buildImage(ctx, b.Config.ContextDir, b.Config.Dockerfile, b.Config.WorkspaceLayerAuth, b.Config.BaseRef)
87
}
88
89
func (b *Builder) buildWorkspaceImage(ctx context.Context) (err error) {
90
log.Info("building workspace image")
91
92
logs.Progress.SetOutput(os.Stderr)
93
94
return crane.Copy(b.Config.BaseRef, b.Config.TargetRef, crane.Insecure, crane.WithJobs(runtime.GOMAXPROCS(0)))
95
}
96
97
func buildImage(ctx context.Context, contextDir, dockerfile, authLayer, target string) (err error) {
98
log.Info("waiting for build context")
99
waitctx, cancel := context.WithTimeout(ctx, 30*time.Minute)
100
defer cancel()
101
102
err = waitForBuildContext(waitctx)
103
if err != nil {
104
return err
105
}
106
107
dockerConfig := "/tmp/config.json"
108
defer os.Remove(dockerConfig)
109
110
if authLayer != "" {
111
configFile := configfile.ConfigFile{
112
AuthConfigs: make(map[string]types.AuthConfig),
113
}
114
115
err := configFile.LoadFromReader(bytes.NewReader([]byte(fmt.Sprintf(`{"auths": %v }`, authLayer))))
116
if err != nil {
117
return xerrors.Errorf("unexpected error reading registry authentication: %w", err)
118
}
119
120
f, _ := os.OpenFile(dockerConfig, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
121
defer f.Close()
122
123
err = configFile.SaveToWriter(f)
124
if err != nil {
125
return xerrors.Errorf("unexpected error writing registry authentication: %w", err)
126
}
127
}
128
129
contextdir := contextDir
130
if contextdir == "" {
131
contextdir = "."
132
}
133
134
buildctlArgs := []string{
135
// "--debug",
136
"build",
137
"--progress=plain",
138
"--output=type=image,name=" + target + ",push=true,oci-mediatypes=true",
139
//"--export-cache=type=inline",
140
"--local=context=" + contextdir,
141
//"--export-cache=type=registry,ref=" + target + "-cache",
142
//"--import-cache=type=registry,ref=" + target + "-cache",
143
"--frontend=dockerfile.v0",
144
"--local=dockerfile=" + filepath.Dir(dockerfile),
145
"--opt=filename=" + filepath.Base(dockerfile),
146
}
147
148
buildctlCmd := exec.Command("buildctl", buildctlArgs...)
149
150
buildctlCmd.Stderr = os.Stderr
151
buildctlCmd.Stdout = os.Stdout
152
153
env := os.Environ()
154
env = append(env, "DOCKER_CONFIG=/tmp")
155
// set log max size to 4MB from 2MB default (to prevent log clipping for large builds)
156
env = append(env, "BUILDKIT_STEP_LOG_MAX_SIZE=4194304")
157
buildctlCmd.Env = env
158
159
if err := buildctlCmd.Start(); err != nil {
160
return err
161
}
162
163
err = buildctlCmd.Wait()
164
if err != nil {
165
return err
166
}
167
168
return nil
169
}
170
171
func waitForBuildContext(ctx context.Context) error {
172
done := make(chan struct{})
173
174
go func() {
175
for {
176
if ctx.Err() != nil {
177
return
178
}
179
180
if _, err := os.Stat("/workspace/.gitpod/ready"); err != nil {
181
continue
182
}
183
184
close(done)
185
return
186
}
187
}()
188
189
select {
190
case <-ctx.Done():
191
return ctx.Err()
192
case <-done:
193
return nil
194
}
195
}
196
197
// StartBuildkit starts a local buildkit daemon
198
func StartBuildkit(socketPath string) (cl *client.Client, teardown func() error, err error) {
199
stderr, err := ioutil.TempFile(os.TempDir(), "buildkitd_stderr")
200
if err != nil {
201
return nil, nil, xerrors.Errorf("cannot create buildkitd log file: %w", err)
202
}
203
stdout, err := ioutil.TempFile(os.TempDir(), "buildkitd_stdout")
204
if err != nil {
205
return nil, nil, xerrors.Errorf("cannot create buildkitd log file: %w", err)
206
}
207
208
cmd := exec.Command("buildkitd",
209
"--debug",
210
"--addr="+socketPath,
211
"--oci-worker-net=host",
212
"--root=/workspace/buildkit",
213
)
214
cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: 0, Gid: 0}}
215
cmd.Stderr = stderr
216
cmd.Stdout = stdout
217
err = cmd.Start()
218
if err != nil {
219
return nil, nil, xerrors.Errorf("cannot start buildkitd: %w", err)
220
}
221
log.WithField("stderr", stderr.Name()).WithField("stdout", stdout.Name()).Debug("buildkitd started")
222
223
defer func() {
224
if err == nil {
225
return
226
}
227
228
if cmd.Process != nil {
229
_ = cmd.Process.Kill()
230
}
231
232
serr, _ := ioutil.ReadFile(stderr.Name())
233
sout, _ := ioutil.ReadFile(stdout.Name())
234
235
log.WithField("buildkitd-stderr", string(serr)).WithField("buildkitd-stdout", string(sout)).Error("buildkitd failure")
236
}()
237
238
teardown = func() error {
239
stdout.Close()
240
stderr.Close()
241
return cmd.Process.Kill()
242
}
243
244
cl, err = connectToBuildkitd(socketPath)
245
if err != nil {
246
return
247
}
248
249
return
250
}
251
252
func connectToBuildkitd(socketPath string) (cl *client.Client, err error) {
253
backoff := 1 * time.Second
254
for i := 0; i < maxConnectionAttempts; i++ {
255
ctx, cancel := context.WithTimeout(context.Background(), initialConnectionTimeout)
256
257
log.WithField("attempt", i).Debug("attempting to connect to buildkitd")
258
cl, err = client.New(ctx, socketPath, client.WithFailFast())
259
if err != nil {
260
cancel()
261
if i == maxConnectionAttempts-1 {
262
log.WithField("attempt", i).WithError(err).Warn("cannot connect to buildkitd")
263
break
264
}
265
266
time.Sleep(backoff)
267
backoff = 2 * backoff
268
continue
269
}
270
271
_, err = cl.ListWorkers(ctx)
272
if err != nil {
273
cancel()
274
if i == maxConnectionAttempts-1 {
275
log.WithField("attempt", i).WithError(err).Error("cannot connect to buildkitd")
276
break
277
}
278
279
time.Sleep(backoff)
280
backoff = 2 * backoff
281
continue
282
}
283
284
cancel()
285
return
286
}
287
288
return nil, xerrors.Errorf("cannot connect to buildkitd")
289
}
290
291