Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-daemon/pkg/content/initializer.go
2500 views
1
// Copyright (c) 2020 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 content
6
7
import (
8
"bytes"
9
"context"
10
"encoding/json"
11
"errors"
12
"io"
13
"os"
14
"os/exec"
15
"path/filepath"
16
"strconv"
17
"strings"
18
"syscall"
19
"time"
20
21
"github.com/google/uuid"
22
"github.com/opencontainers/runtime-spec/specs-go"
23
"github.com/opentracing/opentracing-go"
24
"github.com/sirupsen/logrus"
25
"golang.org/x/xerrors"
26
"google.golang.org/protobuf/proto"
27
28
"github.com/gitpod-io/gitpod/common-go/log"
29
"github.com/gitpod-io/gitpod/common-go/tracing"
30
csapi "github.com/gitpod-io/gitpod/content-service/api"
31
"github.com/gitpod-io/gitpod/content-service/pkg/archive"
32
wsinit "github.com/gitpod-io/gitpod/content-service/pkg/initializer"
33
"github.com/gitpod-io/gitpod/content-service/pkg/storage"
34
"github.com/gitpod-io/gitpod/ws-daemon/pkg/libcontainer/specconv"
35
)
36
37
// RunInitializerOpts configure RunInitializer
38
type RunInitializerOpts struct {
39
// Command is the path to the initializer executable we'll run
40
Command string
41
// Args is a set of additional arguments to pass to the initializer executable
42
Args []string
43
// Options to use on untar
44
IdMappings []archive.IDMapping
45
46
UID uint32
47
GID uint32
48
49
OWI OWI
50
}
51
52
type OWI struct {
53
Owner string
54
WorkspaceID string
55
InstanceID string
56
}
57
58
func (o OWI) Fields() map[string]interface{} {
59
return log.OWI(o.Owner, o.WorkspaceID, o.InstanceID)
60
}
61
62
// errors to be tested with errors.Is
63
var (
64
// cannot find snapshot
65
errCannotFindSnapshot = errors.New("cannot find snapshot")
66
)
67
68
func CollectRemoteContent(ctx context.Context, rs storage.DirectAccess, ps storage.PresignedAccess, workspaceOwner string, initializer *csapi.WorkspaceInitializer) (rc map[string]storage.DownloadInfo, err error) {
69
rc = make(map[string]storage.DownloadInfo)
70
71
backup, err := ps.SignDownload(ctx, rs.Bucket(workspaceOwner), rs.BackupObject(storage.DefaultBackup), &storage.SignedURLOptions{})
72
if err == storage.ErrNotFound {
73
// no backup found - that's fine
74
} else if err != nil {
75
return nil, err
76
} else {
77
rc[storage.DefaultBackup] = *backup
78
}
79
80
si := initializer.GetSnapshot()
81
pi := initializer.GetPrebuild()
82
if ci := initializer.GetComposite(); ci != nil {
83
for _, c := range ci.Initializer {
84
if c.GetSnapshot() != nil {
85
si = c.GetSnapshot()
86
}
87
if c.GetPrebuild() != nil {
88
pi = c.GetPrebuild()
89
}
90
}
91
}
92
if si != nil {
93
bkt, obj, err := storage.ParseSnapshotName(si.Snapshot)
94
if err != nil {
95
return nil, err
96
}
97
info, err := ps.SignDownload(ctx, bkt, obj, &storage.SignedURLOptions{})
98
if err == storage.ErrNotFound {
99
return nil, errCannotFindSnapshot
100
}
101
if err != nil {
102
return nil, xerrors.Errorf("cannot find snapshot: %w", err)
103
}
104
105
rc[si.Snapshot] = *info
106
}
107
if pi != nil && pi.Prebuild != nil && pi.Prebuild.Snapshot != "" {
108
bkt, obj, err := storage.ParseSnapshotName(pi.Prebuild.Snapshot)
109
if err != nil {
110
return nil, err
111
}
112
info, err := ps.SignDownload(ctx, bkt, obj, &storage.SignedURLOptions{})
113
if err == storage.ErrNotFound {
114
// no prebuild found - that's fine
115
} else if err != nil {
116
return nil, xerrors.Errorf("cannot find prebuild: %w", err)
117
} else {
118
rc[pi.Prebuild.Snapshot] = *info
119
}
120
}
121
122
return rc, nil
123
}
124
125
// RunInitializer runs a content initializer in a user, PID and mount namespace to isolate it from ws-daemon
126
func RunInitializer(ctx context.Context, destination string, initializer *csapi.WorkspaceInitializer, remoteContent map[string]storage.DownloadInfo, opts RunInitializerOpts) (*csapi.InitializerMetrics, error) {
127
//nolint:ineffassign,staticcheck
128
span, ctx := opentracing.StartSpanFromContext(ctx, "RunInitializer")
129
var err error
130
defer tracing.FinishSpan(span, &err)
131
132
// it's possible the destination folder doesn't exist yet, because the kubelet hasn't created it yet.
133
// If we fail to create the folder, it either already exists, or we'll fail when we try and mount it.
134
err = os.MkdirAll(destination, 0755)
135
if err != nil && !os.IsExist(err) {
136
return nil, xerrors.Errorf("cannot mkdir destination: %w", err)
137
}
138
139
init, err := proto.Marshal(initializer)
140
if err != nil {
141
return nil, err
142
}
143
144
if opts.GID == 0 {
145
opts.GID = wsinit.GitpodGID
146
}
147
if opts.UID == 0 {
148
opts.UID = wsinit.GitpodUID
149
}
150
151
tmpdir, err := os.MkdirTemp("", "content-init")
152
if err != nil {
153
return nil, err
154
}
155
defer os.RemoveAll(tmpdir)
156
157
err = os.MkdirAll(filepath.Join(tmpdir, "rootfs"), 0755)
158
if err != nil {
159
return nil, err
160
}
161
162
msg := msgInitContent{
163
Destination: "/dst",
164
Initializer: init,
165
RemoteContent: remoteContent,
166
TraceInfo: tracing.GetTraceID(span),
167
IDMappings: opts.IdMappings,
168
GID: int(opts.GID),
169
UID: int(opts.UID),
170
OWI: opts.OWI.Fields(),
171
}
172
fc, err := json.MarshalIndent(msg, "", " ")
173
if err != nil {
174
return nil, err
175
}
176
err = os.WriteFile(filepath.Join(tmpdir, "rootfs", "content.json"), fc, 0644)
177
if err != nil {
178
return nil, err
179
}
180
181
spec := specconv.Example()
182
183
// we assemble the root filesystem from the ws-daemon container
184
for _, d := range []string{"app", "bin", "dev", "etc", "lib", "opt", "sbin", "sys", "usr", "var", "lib32", "lib64", "tmp"} {
185
spec.Mounts = append(spec.Mounts, specs.Mount{
186
Destination: "/" + d,
187
Source: "/" + d,
188
Type: "bind",
189
Options: []string{"rbind", "rprivate"},
190
})
191
}
192
spec.Mounts = append(spec.Mounts, specs.Mount{
193
Destination: "/dst",
194
Source: destination,
195
Type: "bind",
196
Options: []string{"bind", "rprivate"},
197
})
198
199
spec.Hostname = "content-init"
200
spec.Process.Terminal = false
201
spec.Process.NoNewPrivileges = true
202
spec.Process.User.UID = opts.UID
203
spec.Process.User.GID = opts.GID
204
spec.Process.Args = []string{"/app/content-initializer"}
205
for _, e := range os.Environ() {
206
if strings.HasPrefix(e, "JAEGER_") || strings.HasPrefix(e, "GIT_SSL_CAPATH=") || strings.HasPrefix(e, "GIT_SSL_CAINFO=") {
207
spec.Process.Env = append(spec.Process.Env, e)
208
}
209
}
210
211
// TODO(cw): make the initializer work without chown
212
spec.Process.Capabilities.Ambient = append(spec.Process.Capabilities.Ambient, "CAP_CHOWN", "CAP_FOWNER", "CAP_MKNOD", "CAP_SETFCAP")
213
spec.Process.Capabilities.Bounding = append(spec.Process.Capabilities.Bounding, "CAP_CHOWN", "CAP_FOWNER", "CAP_MKNOD", "CAP_SETFCAP")
214
spec.Process.Capabilities.Effective = append(spec.Process.Capabilities.Effective, "CAP_CHOWN", "CAP_FOWNER", "CAP_MKNOD", "CAP_SETFCAP")
215
spec.Process.Capabilities.Inheritable = append(spec.Process.Capabilities.Inheritable, "CAP_CHOWN", "CAP_FOWNER", "CAP_MKNOD", "CAP_SETFCAP")
216
spec.Process.Capabilities.Permitted = append(spec.Process.Capabilities.Permitted, "CAP_CHOWN", "CAP_FOWNER", "CAP_MKNOD", "CAP_SETFCAP")
217
// TODO(cw): setup proper networking in a netns, rather than relying on ws-daemons network
218
n := 0
219
for _, x := range spec.Linux.Namespaces {
220
if x.Type == specs.NetworkNamespace {
221
continue
222
}
223
224
spec.Linux.Namespaces[n] = x
225
n++
226
}
227
spec.Linux.Namespaces = spec.Linux.Namespaces[:n]
228
229
fc, err = json.MarshalIndent(spec, "", " ")
230
if err != nil {
231
return nil, err
232
}
233
err = os.WriteFile(filepath.Join(tmpdir, "config.json"), fc, 0644)
234
if err != nil {
235
return nil, err
236
}
237
238
args := []string{"--root", "state"}
239
240
if log.Log.Logger.IsLevelEnabled(logrus.DebugLevel) {
241
args = append(args, "--debug")
242
}
243
244
var name string
245
if opts.OWI.InstanceID == "" {
246
id, err := uuid.NewRandom()
247
if err != nil {
248
return nil, err
249
}
250
name = "init-rnd-" + id.String()
251
} else {
252
name = "init-ws-" + opts.OWI.InstanceID
253
}
254
255
// pass a pipe "file" to the content init process as fd 3 to capture the error output
256
errIn, errOut, err := os.Pipe()
257
if err != nil {
258
return nil, err
259
}
260
261
// pass a pipe "file" to the content init process as fd 4 to capture the metrics output
262
statsIn, statsOut, err := os.Pipe()
263
if err != nil {
264
return nil, err
265
}
266
267
args = append(args, "--log-format", "json", "run")
268
extraFiles := []*os.File{errOut, statsOut}
269
args = append(args, "--preserve-fds", strconv.Itoa(len(extraFiles)))
270
args = append(args, name)
271
272
var cmdOut bytes.Buffer
273
cmd := exec.Command("runc", args...)
274
cmd.Dir = tmpdir
275
cmd.Stdout = &cmdOut
276
cmd.Stderr = os.Stderr
277
cmd.Stdin = os.Stdin
278
cmd.ExtraFiles = extraFiles
279
err = cmd.Run()
280
log.FromBuffer(&cmdOut, log.WithFields(opts.OWI.Fields()))
281
282
// read contents of the extra files
283
errOut.Close()
284
statsOut.Close()
285
errmsg, statsBytes := waitForAndReadExtraFiles(errIn, statsIn)
286
if err != nil {
287
if exiterr, ok := err.(*exec.ExitError); ok {
288
// The program has exited with an exit code != 0. If it's FAIL_CONTENT_INITIALIZER_EXIT_CODE, it was deliberate.
289
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok && status.ExitStatus() == FAIL_CONTENT_INITIALIZER_EXIT_CODE {
290
log.WithError(err).WithFields(opts.OWI.Fields()).WithField("errmsgsize", len(errmsg)).WithField("exitCode", status.ExitStatus()).WithField("args", args).Error("content init failed")
291
return nil, xerrors.Errorf(string(errmsg))
292
}
293
}
294
295
return nil, err
296
}
297
298
stats := parseStats(statsBytes)
299
return stats, nil
300
}
301
302
// waitForAndReadExtraFiles tries to read the content of the extra files passed to the content initializer, and waits up to 1s to do so
303
func waitForAndReadExtraFiles(errIn *os.File, statsIn *os.File) (errmsg []byte, statsBytes []byte) {
304
// read err
305
errch := make(chan []byte, 1)
306
go func() {
307
errmsg, _ := io.ReadAll(errIn)
308
errch <- errmsg
309
}()
310
311
// read stats
312
statsCh := make(chan []byte, 1)
313
go func() {
314
statsBytes, readErr := io.ReadAll(statsIn)
315
if readErr != nil {
316
log.WithError(readErr).Warn("cannot read stats")
317
}
318
log.WithField("statsBytes", log.TrustedValueWrap{Value: string(statsBytes)}).Debug("read stats")
319
statsCh <- statsBytes
320
}()
321
322
readFiles := 0
323
for {
324
select {
325
case errmsg = <-errch:
326
readFiles += 1
327
case statsBytes = <-statsCh:
328
readFiles += 1
329
case <-time.After(1 * time.Second):
330
if errmsg == nil {
331
errmsg = []byte("failed to read content initializer response")
332
}
333
return
334
}
335
if readFiles == 2 {
336
return
337
}
338
}
339
}
340
341
func parseStats(statsBytes []byte) *csapi.InitializerMetrics {
342
var stats csapi.InitializerMetrics
343
err := json.Unmarshal(statsBytes, &stats)
344
if err != nil {
345
log.WithError(err).WithField("bytes", log.TrustedValueWrap{Value: statsBytes}).Warn("cannot unmarshal stats")
346
return nil
347
}
348
return &stats
349
}
350
351
// RUN_INITIALIZER_CHILD_ERROUT_FD is the fileDescriptor of the "errout" file descriptor passed to the content initializer
352
const RUN_INITIALIZER_CHILD_ERROUT_FD = 3
353
354
// RUN_INITIALIZER_CHILD_STATS_FD is the fileDescriptor of the "stats" file descriptor passed to the content initializer
355
const RUN_INITIALIZER_CHILD_STATS_FD = 4
356
357
// RunInitializerChild is the function that's expected to run when we call `/proc/self/exe content-initializer`
358
func RunInitializerChild(statsFd *os.File) (err error) {
359
fc, err := os.ReadFile("/content.json")
360
if err != nil {
361
return err
362
}
363
364
var initmsg msgInitContent
365
err = json.Unmarshal(fc, &initmsg)
366
if err != nil {
367
return err
368
}
369
log.Log = logrus.WithFields(initmsg.OWI)
370
371
defer func() {
372
if err != nil {
373
log.WithError(err).WithFields(initmsg.OWI).Error("content init failed")
374
}
375
}()
376
377
span := opentracing.StartSpan("RunInitializerChild", opentracing.ChildOf(tracing.FromTraceID(initmsg.TraceInfo)))
378
defer tracing.FinishSpan(span, &err)
379
ctx := opentracing.ContextWithSpan(context.Background(), span)
380
381
var req csapi.WorkspaceInitializer
382
err = proto.Unmarshal(initmsg.Initializer, &req)
383
if err != nil {
384
return err
385
}
386
387
rs := &remoteContentStorage{RemoteContent: initmsg.RemoteContent}
388
389
dst := initmsg.Destination
390
initializer, err := wsinit.NewFromRequest(ctx, dst, rs, &req, wsinit.NewFromRequestOpts{ForceGitpodUserForGit: false})
391
if err != nil {
392
return err
393
}
394
395
initSource, stats, err := wsinit.InitializeWorkspace(ctx, dst, rs,
396
wsinit.WithInitializer(initializer),
397
wsinit.WithMappings(initmsg.IDMappings),
398
wsinit.WithChown(initmsg.UID, initmsg.GID),
399
wsinit.WithCleanSlate,
400
)
401
if err != nil {
402
return err
403
}
404
405
// some workspace content may have a `/dst/.gitpod` file or directory. That would break
406
// the workspace ready file placement (see https://github.com/gitpod-io/gitpod/issues/7694).
407
err = wsinit.EnsureCleanDotGitpodDirectory(ctx, dst)
408
if err != nil {
409
return err
410
}
411
412
// Place the ready file to make Theia "open its gates"
413
err = wsinit.PlaceWorkspaceReadyFile(ctx, dst, initSource, stats, initmsg.UID, initmsg.GID)
414
if err != nil {
415
return err
416
}
417
418
// Serialize metrics, so we can pass them back to the caller
419
if statsFd != nil {
420
defer statsFd.Close()
421
writeInitializerStats(statsFd, &stats)
422
} else {
423
log.Warn("no stats file descriptor provided")
424
}
425
426
return nil
427
}
428
429
func writeInitializerStats(statsFd *os.File, stats *csapi.InitializerMetrics) {
430
serializedStats, err := json.Marshal(stats)
431
if err != nil {
432
log.WithError(err).Warn("cannot serialize initializer stats")
433
return
434
}
435
436
log.WithField("stats", log.TrustedValueWrap{Value: string(serializedStats)}).Debug("writing initializer stats to fd")
437
_, writeErr := statsFd.Write(serializedStats)
438
if writeErr != nil {
439
log.WithError(writeErr).Warn("error writing initializer stats to fd")
440
return
441
}
442
}
443
444
var _ storage.DirectAccess = &remoteContentStorage{}
445
446
type remoteContentStorage struct {
447
RemoteContent map[string]storage.DownloadInfo
448
}
449
450
// Init does nothing
451
func (rs *remoteContentStorage) Init(ctx context.Context, owner, workspace, instance string) error {
452
return nil
453
}
454
455
// EnsureExists does nothing
456
func (rs *remoteContentStorage) EnsureExists(ctx context.Context) error {
457
return nil
458
}
459
460
// Download always returns false and does nothing
461
func (rs *remoteContentStorage) Download(ctx context.Context, destination string, name string, mappings []archive.IDMapping) (exists bool, err error) {
462
span, ctx := opentracing.StartSpanFromContext(ctx, "remoteContentStorage.Download")
463
span.SetTag("destination", destination)
464
span.SetTag("name", name)
465
defer tracing.FinishSpan(span, &err)
466
467
info, exists := rs.RemoteContent[name]
468
if !exists {
469
return false, nil
470
}
471
472
span.SetTag("URL", info.URL)
473
474
// create a temporal file to download the content
475
tempFile, err := os.CreateTemp("", "remote-content-*")
476
if err != nil {
477
return true, xerrors.Errorf("cannot create temporal file: %w", err)
478
}
479
tempFile.Close()
480
481
args := []string{
482
"-s10", "-x16", "-j12",
483
"--retry-wait=5",
484
"--log-level=error",
485
"--allow-overwrite=true", // rewrite temporal empty file
486
info.URL,
487
"-o", tempFile.Name(),
488
}
489
490
downloadStart := time.Now()
491
cmd := exec.Command("aria2c", args...)
492
out, err := cmd.CombinedOutput()
493
if err != nil {
494
log.WithError(err).WithField("out", string(out)).Error("unexpected error downloading file")
495
return true, xerrors.Errorf("unexpected error downloading file")
496
}
497
downloadDuration := time.Since(downloadStart)
498
log.WithField("downloadDuration", downloadDuration.String()).Info("aria2c download duration")
499
500
tempFile, err = os.Open(tempFile.Name())
501
if err != nil {
502
return true, xerrors.Errorf("unexpected error downloading file")
503
}
504
505
defer os.Remove(tempFile.Name())
506
defer tempFile.Close()
507
508
extractStart := time.Now()
509
err = archive.ExtractTarbal(ctx, tempFile, destination, archive.WithUIDMapping(mappings), archive.WithGIDMapping(mappings))
510
if err != nil {
511
return true, xerrors.Errorf("tar %s: %s", destination, err.Error())
512
}
513
extractDuration := time.Since(extractStart)
514
log.WithField("extractDuration", extractDuration.String()).Info("extract tarbal duration")
515
516
return true, nil
517
}
518
519
// DownloadSnapshot always returns false and does nothing
520
func (rs *remoteContentStorage) DownloadSnapshot(ctx context.Context, destination string, name string, mappings []archive.IDMapping) (bool, error) {
521
return rs.Download(ctx, destination, name, mappings)
522
}
523
524
// ListObjects returns all objects found with the given prefix. Returns an empty list if the bucket does not exuist (yet).
525
func (rs *remoteContentStorage) ListObjects(ctx context.Context, prefix string) (objects []string, err error) {
526
return []string{}, nil
527
}
528
529
// Qualify just returns the name
530
func (rs *remoteContentStorage) Qualify(name string) string {
531
return name
532
}
533
534
// Upload does nothing
535
func (rs *remoteContentStorage) Upload(ctx context.Context, source string, name string, opts ...storage.UploadOption) (string, string, error) {
536
return "", "", xerrors.Errorf("not implemented")
537
}
538
539
// UploadInstance takes all files from a local location and uploads it to the remote storage
540
func (rs *remoteContentStorage) UploadInstance(ctx context.Context, source string, name string, options ...storage.UploadOption) (bucket, obj string, err error) {
541
return "", "", xerrors.Errorf("not implemented")
542
}
543
544
// Bucket returns an empty string
545
func (rs *remoteContentStorage) Bucket(string) string {
546
return ""
547
}
548
549
// BackupObject returns a backup's object name that a direct downloader would download
550
func (rs *remoteContentStorage) BackupObject(name string) string {
551
return ""
552
}
553
554
// InstanceObject returns a instance's object name that a direct downloader would download
555
func (rs *remoteContentStorage) InstanceObject(workspaceID string, instanceID string, name string) string {
556
return ""
557
}
558
559
// SnapshotObject returns a snapshot's object name that a direct downloer would download
560
func (rs *remoteContentStorage) SnapshotObject(name string) string {
561
return ""
562
}
563
564
type msgInitContent struct {
565
Destination string
566
RemoteContent map[string]storage.DownloadInfo
567
Initializer []byte
568
UID, GID int
569
IDMappings []archive.IDMapping
570
571
TraceInfo string
572
OWI map[string]interface{}
573
}
574
575