Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/content-service/pkg/layer/provider.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 layer
6
7
import (
8
"archive/tar"
9
"bytes"
10
"context"
11
"encoding/json"
12
"fmt"
13
"io"
14
"net/http"
15
"strings"
16
17
"github.com/opencontainers/go-digest"
18
"github.com/opentracing/opentracing-go"
19
"golang.org/x/xerrors"
20
21
"github.com/gitpod-io/gitpod/common-go/log"
22
"github.com/gitpod-io/gitpod/common-go/tracing"
23
csapi "github.com/gitpod-io/gitpod/content-service/api"
24
"github.com/gitpod-io/gitpod/content-service/api/config"
25
"github.com/gitpod-io/gitpod/content-service/pkg/executor"
26
"github.com/gitpod-io/gitpod/content-service/pkg/initializer"
27
"github.com/gitpod-io/gitpod/content-service/pkg/storage"
28
)
29
30
const (
31
// BEWARE:
32
// these formats duplicate naming conventions embedded in the remote storage implementations or ws-daemon.
33
fmtWorkspaceManifest = "workspaces/%s/wsfull.json"
34
fmtLegacyBackupName = "workspaces/%s/full.tar"
35
)
36
37
// NewProvider produces a new content layer provider
38
func NewProvider(cfg *config.StorageConfig) (*Provider, error) {
39
s, err := storage.NewPresignedAccess(cfg)
40
if err != nil {
41
return nil, err
42
}
43
return &Provider{
44
Storage: s,
45
Client: &http.Client{},
46
}, nil
47
}
48
49
// Provider provides access to a workspace's content
50
type Provider struct {
51
Storage storage.PresignedAccess
52
Client *http.Client
53
}
54
55
var errUnsupportedContentType = xerrors.Errorf("unsupported workspace content type")
56
57
func (s *Provider) downloadContentManifest(ctx context.Context, bkt, obj string) (manifest *csapi.WorkspaceContentManifest, info *storage.DownloadInfo, err error) {
58
//nolint:ineffassign
59
span, ctx := opentracing.StartSpanFromContext(ctx, "downloadContentManifest")
60
defer func() {
61
var lerr error
62
if manifest != nil {
63
lerr = err
64
if lerr == storage.ErrNotFound {
65
span.LogKV("found", false)
66
lerr = nil
67
}
68
}
69
70
tracing.FinishSpan(span, &lerr)
71
}()
72
73
info, err = s.Storage.SignDownload(ctx, bkt, obj, &storage.SignedURLOptions{})
74
if err != nil {
75
return
76
}
77
if info.Meta.ContentType != csapi.ContentTypeManifest {
78
err = errUnsupportedContentType
79
return
80
}
81
82
mfreq, err := http.NewRequestWithContext(ctx, "GET", info.URL, nil)
83
if err != nil {
84
return
85
}
86
mfresp, err := s.Client.Do(mfreq)
87
if err != nil {
88
return
89
}
90
if mfresp.StatusCode != http.StatusOK {
91
err = xerrors.Errorf("cannot get %s: status %d", info.URL, mfresp.StatusCode)
92
return
93
}
94
if mfresp.Body == nil {
95
err = xerrors.Errorf("empty response")
96
return
97
}
98
defer mfresp.Body.Close()
99
100
mfr, err := io.ReadAll(mfresp.Body)
101
if err != nil {
102
return
103
}
104
105
var mf csapi.WorkspaceContentManifest
106
err = json.Unmarshal(mfr, &mf)
107
if err != nil {
108
return
109
}
110
manifest = &mf
111
112
err = errUnsupportedContentType
113
return
114
}
115
116
// GetContentLayer provides the content layer for a workspace
117
func (s *Provider) GetContentLayer(ctx context.Context, owner, workspaceID string, initializer *csapi.WorkspaceInitializer) (l []Layer, manifest *csapi.WorkspaceContentManifest, err error) {
118
span, ctx := tracing.FromContext(ctx, "GetContentLayer")
119
defer tracing.FinishSpan(span, &err)
120
tracing.ApplyOWI(span, log.OWI(owner, workspaceID, ""))
121
122
defer func() {
123
// we never return a nil manifest, just maybe an empty one
124
if manifest == nil {
125
manifest = &csapi.WorkspaceContentManifest{}
126
}
127
}()
128
129
// check if workspace has an FWB
130
var (
131
bucket = s.Storage.Bucket(owner)
132
mfobj = fmt.Sprintf(fmtWorkspaceManifest, workspaceID)
133
)
134
span.LogKV("bucket", bucket, "mfobj", mfobj)
135
manifest, _, err = s.downloadContentManifest(ctx, bucket, mfobj)
136
if err != nil && err != storage.ErrNotFound {
137
return nil, nil, err
138
}
139
if manifest != nil {
140
span.LogKV("backup found", "full workspace backup")
141
142
l, err = s.layerFromContentManifest(ctx, manifest, csapi.WorkspaceInitFromBackup, true)
143
return l, manifest, err
144
}
145
146
// check if legacy workspace backup is present
147
var layer *Layer
148
info, err := s.Storage.SignDownload(ctx, bucket, fmt.Sprintf(fmtLegacyBackupName, workspaceID), &storage.SignedURLOptions{})
149
if err != nil && !xerrors.Is(err, storage.ErrNotFound) {
150
return nil, nil, err
151
}
152
if err == nil {
153
span.LogKV("backup found", "legacy workspace backup")
154
155
cdesc, err := executor.PrepareFromBackup(info.URL)
156
if err != nil {
157
return nil, nil, err
158
}
159
160
layer, err = contentDescriptorToLayer(cdesc)
161
if err != nil {
162
return nil, nil, err
163
}
164
165
l = []Layer{*layer}
166
return l, manifest, nil
167
}
168
169
// At this point we've found neither a full-workspace-backup, nor a legacy backup.
170
// It's time to use the initializer.
171
if gis := initializer.GetSnapshot(); gis != nil {
172
return s.getSnapshotContentLayer(ctx, gis)
173
}
174
if pis := initializer.GetPrebuild(); pis != nil {
175
l, manifest, err = s.getPrebuildContentLayer(ctx, pis)
176
if err != nil {
177
log.WithError(err).WithFields(log.OWI(owner, workspaceID, "")).Warn("cannot initialize from prebuild - falling back to Git")
178
span.LogKV("fallback-to-git", err.Error())
179
180
// we failed creating a prebuild initializer, so let's try falling back to the Git part.
181
var init []*csapi.WorkspaceInitializer
182
for _, gi := range pis.Git {
183
init = append(init, &csapi.WorkspaceInitializer{
184
Spec: &csapi.WorkspaceInitializer_Git{
185
Git: gi,
186
},
187
})
188
}
189
initializer = &csapi.WorkspaceInitializer{
190
Spec: &csapi.WorkspaceInitializer_Composite{
191
Composite: &csapi.CompositeInitializer{
192
Initializer: init,
193
},
194
},
195
}
196
} else {
197
// creating the initializer worked - we're done here
198
return
199
}
200
}
201
if gis := initializer.GetGit(); gis != nil {
202
span.LogKV("initializer", "Git")
203
204
cdesc, err := executor.Prepare(initializer, nil)
205
if err != nil {
206
return nil, nil, err
207
}
208
209
layer, err = contentDescriptorToLayer(cdesc)
210
if err != nil {
211
return nil, nil, err
212
}
213
return []Layer{*layer}, nil, nil
214
}
215
if initializer.GetBackup() != nil {
216
// We were asked to restore a backup and have tried above. We've failed to restore the backup,
217
// hance the backup initializer failed.
218
return nil, nil, xerrors.Errorf("no backup found")
219
}
220
221
return nil, nil, xerrors.Errorf("no backup or valid initializer present")
222
}
223
224
func (s *Provider) getSnapshotContentLayer(ctx context.Context, sp *csapi.SnapshotInitializer) (l []Layer, manifest *csapi.WorkspaceContentManifest, err error) {
225
span, ctx := tracing.FromContext(ctx, "getSnapshotContentLayer")
226
defer tracing.FinishSpan(span, &err)
227
228
segs := strings.Split(sp.Snapshot, "@")
229
if len(segs) != 2 {
230
return nil, nil, xerrors.Errorf("invalid snapshot FQN: %s", sp.Snapshot)
231
}
232
obj, bkt := segs[0], segs[1]
233
234
// maybe the snapshot is a full workspace snapshot, i.e. has a content manifest
235
manifest, info, err := s.downloadContentManifest(ctx, bkt, obj)
236
if err == storage.ErrNotFound {
237
return nil, nil, xerrors.Errorf("invalid snapshot: %w", err)
238
}
239
// If err == errUnsupportedContentType we've found a storage object but with invalid type.
240
// Chances are we have a non-fwb snapshot at our hands.
241
if err != nil && err != errUnsupportedContentType {
242
return nil, nil, err
243
}
244
245
if manifest == nil {
246
// we've found a legacy snapshot
247
cdesc, err := executor.Prepare(&csapi.WorkspaceInitializer{Spec: &csapi.WorkspaceInitializer_Snapshot{Snapshot: sp}}, map[string]string{
248
sp.Snapshot: info.URL,
249
})
250
if err != nil {
251
return nil, nil, err
252
}
253
254
layer, err := contentDescriptorToLayer(cdesc)
255
if err != nil {
256
return nil, nil, err
257
}
258
return []Layer{*layer}, nil, nil
259
}
260
261
// we've found a manifest for this fwb snapshot - let's use it
262
l, err = s.layerFromContentManifest(ctx, manifest, csapi.WorkspaceInitFromOther, true)
263
return l, manifest, nil
264
}
265
266
func (s *Provider) getPrebuildContentLayer(ctx context.Context, pb *csapi.PrebuildInitializer) (l []Layer, manifest *csapi.WorkspaceContentManifest, err error) {
267
span, ctx := tracing.FromContext(ctx, "getPrebuildContentLayer")
268
defer tracing.FinishSpan(span, &err)
269
270
segs := strings.Split(pb.Prebuild.Snapshot, "@")
271
if len(segs) != 2 {
272
return nil, nil, xerrors.Errorf("invalid snapshot FQN: %s", pb.Prebuild.Snapshot)
273
}
274
obj, bkt := segs[0], segs[1]
275
276
// maybe the snapshot is a full workspace snapshot, i.e. has a content manifest
277
manifest, info, err := s.downloadContentManifest(ctx, bkt, obj)
278
if err == storage.ErrNotFound {
279
return nil, nil, xerrors.Errorf("invalid snapshot: %w", err)
280
}
281
282
// If err == errUnsupportedContentType we've found a storage object but with invalid type.
283
// Chances are we have a non-fwb snapshot at our hands.
284
if err != nil && err != errUnsupportedContentType {
285
return nil, nil, err
286
}
287
288
var cdesc []byte
289
if manifest == nil {
290
// legacy prebuild - resort to in-workspace content init
291
cdesc, err = executor.Prepare(&csapi.WorkspaceInitializer{Spec: &csapi.WorkspaceInitializer_Prebuild{Prebuild: pb}}, map[string]string{
292
pb.Prebuild.Snapshot: info.URL,
293
})
294
if err != nil {
295
return nil, nil, err
296
}
297
} else {
298
// fwb prebuild - add snapshot as content layer
299
var ls []Layer
300
ls, err = s.layerFromContentManifest(ctx, manifest, csapi.WorkspaceInitFromPrebuild, false)
301
if err != nil {
302
return nil, nil, err
303
}
304
l = append(l, ls...)
305
306
// and run no-snapshot prebuild init in workspace
307
cdesc, err = executor.Prepare(&csapi.WorkspaceInitializer{
308
Spec: &csapi.WorkspaceInitializer_Prebuild{
309
Prebuild: &csapi.PrebuildInitializer{
310
Git: pb.Git,
311
},
312
},
313
}, nil)
314
if err != nil {
315
return nil, nil, err
316
}
317
}
318
319
layer, err := contentDescriptorToLayer(cdesc)
320
321
if err != nil {
322
return nil, nil, err
323
}
324
l = append(l, *layer)
325
return l, manifest, nil
326
}
327
328
func (s *Provider) layerFromContentManifest(ctx context.Context, mf *csapi.WorkspaceContentManifest, initsrc csapi.WorkspaceInitSource, ready bool) (l []Layer, err error) {
329
// we have a valid full workspace backup
330
l = make([]Layer, len(mf.Layers))
331
for i, mfl := range mf.Layers {
332
info, err := s.Storage.SignDownload(ctx, mfl.Bucket, mfl.Object, &storage.SignedURLOptions{})
333
if err != nil {
334
return nil, err
335
}
336
if info.Meta.Digest != mfl.Digest.String() {
337
return nil, xerrors.Errorf("digest mismatch for %s/%s: expected %s, got %s", mfl.Bucket, mfl.Object, mfl.Digest, info.Meta.Digest)
338
}
339
l[i] = Layer{
340
DiffID: mfl.DiffID.String(),
341
Digest: mfl.Digest.String(),
342
MediaType: mfl.MediaType,
343
URL: info.URL,
344
Size: mfl.Size,
345
}
346
}
347
348
if ready {
349
rl, err := workspaceReadyLayer(initsrc)
350
if err != nil {
351
return nil, err
352
}
353
l = append(l, *rl)
354
}
355
return l, nil
356
}
357
358
func contentDescriptorToLayer(cdesc []byte) (*Layer, error) {
359
return layerFromContent(
360
fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/workspace", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},
361
fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/workspace/.gitpod", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},
362
fileInLayer{&tar.Header{Typeflag: tar.TypeReg, Name: "/workspace/.gitpod/content.json", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755, Size: int64(len(cdesc))}, cdesc},
363
)
364
}
365
366
func workspaceReadyLayer(src csapi.WorkspaceInitSource) (*Layer, error) {
367
msg := csapi.WorkspaceReadyMessage{
368
Source: src,
369
}
370
ctnt, err := json.Marshal(msg)
371
if err != nil {
372
return nil, err
373
}
374
375
return layerFromContent(
376
fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/workspace", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},
377
fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/workspace/.gitpod", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},
378
fileInLayer{&tar.Header{Typeflag: tar.TypeReg, Name: "/workspace/.gitpod/ready", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755, Size: int64(len(ctnt))}, []byte(ctnt)},
379
)
380
}
381
382
type fileInLayer struct {
383
Header *tar.Header
384
Content []byte
385
}
386
387
func layerFromContent(fs ...fileInLayer) (*Layer, error) {
388
buf := bytes.NewBuffer(nil)
389
tw := tar.NewWriter(buf)
390
for _, h := range fs {
391
err := tw.WriteHeader(h.Header)
392
if err != nil {
393
return nil, xerrors.Errorf("cannot prepare content layer: %w", err)
394
}
395
396
if len(h.Content) == 0 {
397
continue
398
}
399
_, err = tw.Write(h.Content)
400
if err != nil {
401
return nil, xerrors.Errorf("cannot prepare content layer: %w", err)
402
}
403
}
404
tw.Close()
405
406
return &Layer{
407
Digest: digest.FromBytes(buf.Bytes()).String(),
408
Content: buf.Bytes(),
409
}, nil
410
}
411
412
// Layer is a content layer which is meant to be added to a workspace's image
413
type Layer struct {
414
Content []byte
415
416
URL string
417
Digest string
418
DiffID string
419
MediaType string
420
Size int64
421
}
422
423