Path: blob/main/components/content-service/pkg/layer/provider.go
2500 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package layer56import (7"archive/tar"8"bytes"9"context"10"encoding/json"11"fmt"12"io"13"net/http"14"strings"1516"github.com/opencontainers/go-digest"17"github.com/opentracing/opentracing-go"18"golang.org/x/xerrors"1920"github.com/gitpod-io/gitpod/common-go/log"21"github.com/gitpod-io/gitpod/common-go/tracing"22csapi "github.com/gitpod-io/gitpod/content-service/api"23"github.com/gitpod-io/gitpod/content-service/api/config"24"github.com/gitpod-io/gitpod/content-service/pkg/executor"25"github.com/gitpod-io/gitpod/content-service/pkg/initializer"26"github.com/gitpod-io/gitpod/content-service/pkg/storage"27)2829const (30// BEWARE:31// these formats duplicate naming conventions embedded in the remote storage implementations or ws-daemon.32fmtWorkspaceManifest = "workspaces/%s/wsfull.json"33fmtLegacyBackupName = "workspaces/%s/full.tar"34)3536// NewProvider produces a new content layer provider37func NewProvider(cfg *config.StorageConfig) (*Provider, error) {38s, err := storage.NewPresignedAccess(cfg)39if err != nil {40return nil, err41}42return &Provider{43Storage: s,44Client: &http.Client{},45}, nil46}4748// Provider provides access to a workspace's content49type Provider struct {50Storage storage.PresignedAccess51Client *http.Client52}5354var errUnsupportedContentType = xerrors.Errorf("unsupported workspace content type")5556func (s *Provider) downloadContentManifest(ctx context.Context, bkt, obj string) (manifest *csapi.WorkspaceContentManifest, info *storage.DownloadInfo, err error) {57//nolint:ineffassign58span, ctx := opentracing.StartSpanFromContext(ctx, "downloadContentManifest")59defer func() {60var lerr error61if manifest != nil {62lerr = err63if lerr == storage.ErrNotFound {64span.LogKV("found", false)65lerr = nil66}67}6869tracing.FinishSpan(span, &lerr)70}()7172info, err = s.Storage.SignDownload(ctx, bkt, obj, &storage.SignedURLOptions{})73if err != nil {74return75}76if info.Meta.ContentType != csapi.ContentTypeManifest {77err = errUnsupportedContentType78return79}8081mfreq, err := http.NewRequestWithContext(ctx, "GET", info.URL, nil)82if err != nil {83return84}85mfresp, err := s.Client.Do(mfreq)86if err != nil {87return88}89if mfresp.StatusCode != http.StatusOK {90err = xerrors.Errorf("cannot get %s: status %d", info.URL, mfresp.StatusCode)91return92}93if mfresp.Body == nil {94err = xerrors.Errorf("empty response")95return96}97defer mfresp.Body.Close()9899mfr, err := io.ReadAll(mfresp.Body)100if err != nil {101return102}103104var mf csapi.WorkspaceContentManifest105err = json.Unmarshal(mfr, &mf)106if err != nil {107return108}109manifest = &mf110111err = errUnsupportedContentType112return113}114115// GetContentLayer provides the content layer for a workspace116func (s *Provider) GetContentLayer(ctx context.Context, owner, workspaceID string, initializer *csapi.WorkspaceInitializer) (l []Layer, manifest *csapi.WorkspaceContentManifest, err error) {117span, ctx := tracing.FromContext(ctx, "GetContentLayer")118defer tracing.FinishSpan(span, &err)119tracing.ApplyOWI(span, log.OWI(owner, workspaceID, ""))120121defer func() {122// we never return a nil manifest, just maybe an empty one123if manifest == nil {124manifest = &csapi.WorkspaceContentManifest{}125}126}()127128// check if workspace has an FWB129var (130bucket = s.Storage.Bucket(owner)131mfobj = fmt.Sprintf(fmtWorkspaceManifest, workspaceID)132)133span.LogKV("bucket", bucket, "mfobj", mfobj)134manifest, _, err = s.downloadContentManifest(ctx, bucket, mfobj)135if err != nil && err != storage.ErrNotFound {136return nil, nil, err137}138if manifest != nil {139span.LogKV("backup found", "full workspace backup")140141l, err = s.layerFromContentManifest(ctx, manifest, csapi.WorkspaceInitFromBackup, true)142return l, manifest, err143}144145// check if legacy workspace backup is present146var layer *Layer147info, err := s.Storage.SignDownload(ctx, bucket, fmt.Sprintf(fmtLegacyBackupName, workspaceID), &storage.SignedURLOptions{})148if err != nil && !xerrors.Is(err, storage.ErrNotFound) {149return nil, nil, err150}151if err == nil {152span.LogKV("backup found", "legacy workspace backup")153154cdesc, err := executor.PrepareFromBackup(info.URL)155if err != nil {156return nil, nil, err157}158159layer, err = contentDescriptorToLayer(cdesc)160if err != nil {161return nil, nil, err162}163164l = []Layer{*layer}165return l, manifest, nil166}167168// At this point we've found neither a full-workspace-backup, nor a legacy backup.169// It's time to use the initializer.170if gis := initializer.GetSnapshot(); gis != nil {171return s.getSnapshotContentLayer(ctx, gis)172}173if pis := initializer.GetPrebuild(); pis != nil {174l, manifest, err = s.getPrebuildContentLayer(ctx, pis)175if err != nil {176log.WithError(err).WithFields(log.OWI(owner, workspaceID, "")).Warn("cannot initialize from prebuild - falling back to Git")177span.LogKV("fallback-to-git", err.Error())178179// we failed creating a prebuild initializer, so let's try falling back to the Git part.180var init []*csapi.WorkspaceInitializer181for _, gi := range pis.Git {182init = append(init, &csapi.WorkspaceInitializer{183Spec: &csapi.WorkspaceInitializer_Git{184Git: gi,185},186})187}188initializer = &csapi.WorkspaceInitializer{189Spec: &csapi.WorkspaceInitializer_Composite{190Composite: &csapi.CompositeInitializer{191Initializer: init,192},193},194}195} else {196// creating the initializer worked - we're done here197return198}199}200if gis := initializer.GetGit(); gis != nil {201span.LogKV("initializer", "Git")202203cdesc, err := executor.Prepare(initializer, nil)204if err != nil {205return nil, nil, err206}207208layer, err = contentDescriptorToLayer(cdesc)209if err != nil {210return nil, nil, err211}212return []Layer{*layer}, nil, nil213}214if initializer.GetBackup() != nil {215// We were asked to restore a backup and have tried above. We've failed to restore the backup,216// hance the backup initializer failed.217return nil, nil, xerrors.Errorf("no backup found")218}219220return nil, nil, xerrors.Errorf("no backup or valid initializer present")221}222223func (s *Provider) getSnapshotContentLayer(ctx context.Context, sp *csapi.SnapshotInitializer) (l []Layer, manifest *csapi.WorkspaceContentManifest, err error) {224span, ctx := tracing.FromContext(ctx, "getSnapshotContentLayer")225defer tracing.FinishSpan(span, &err)226227segs := strings.Split(sp.Snapshot, "@")228if len(segs) != 2 {229return nil, nil, xerrors.Errorf("invalid snapshot FQN: %s", sp.Snapshot)230}231obj, bkt := segs[0], segs[1]232233// maybe the snapshot is a full workspace snapshot, i.e. has a content manifest234manifest, info, err := s.downloadContentManifest(ctx, bkt, obj)235if err == storage.ErrNotFound {236return nil, nil, xerrors.Errorf("invalid snapshot: %w", err)237}238// If err == errUnsupportedContentType we've found a storage object but with invalid type.239// Chances are we have a non-fwb snapshot at our hands.240if err != nil && err != errUnsupportedContentType {241return nil, nil, err242}243244if manifest == nil {245// we've found a legacy snapshot246cdesc, err := executor.Prepare(&csapi.WorkspaceInitializer{Spec: &csapi.WorkspaceInitializer_Snapshot{Snapshot: sp}}, map[string]string{247sp.Snapshot: info.URL,248})249if err != nil {250return nil, nil, err251}252253layer, err := contentDescriptorToLayer(cdesc)254if err != nil {255return nil, nil, err256}257return []Layer{*layer}, nil, nil258}259260// we've found a manifest for this fwb snapshot - let's use it261l, err = s.layerFromContentManifest(ctx, manifest, csapi.WorkspaceInitFromOther, true)262return l, manifest, nil263}264265func (s *Provider) getPrebuildContentLayer(ctx context.Context, pb *csapi.PrebuildInitializer) (l []Layer, manifest *csapi.WorkspaceContentManifest, err error) {266span, ctx := tracing.FromContext(ctx, "getPrebuildContentLayer")267defer tracing.FinishSpan(span, &err)268269segs := strings.Split(pb.Prebuild.Snapshot, "@")270if len(segs) != 2 {271return nil, nil, xerrors.Errorf("invalid snapshot FQN: %s", pb.Prebuild.Snapshot)272}273obj, bkt := segs[0], segs[1]274275// maybe the snapshot is a full workspace snapshot, i.e. has a content manifest276manifest, info, err := s.downloadContentManifest(ctx, bkt, obj)277if err == storage.ErrNotFound {278return nil, nil, xerrors.Errorf("invalid snapshot: %w", err)279}280281// If err == errUnsupportedContentType we've found a storage object but with invalid type.282// Chances are we have a non-fwb snapshot at our hands.283if err != nil && err != errUnsupportedContentType {284return nil, nil, err285}286287var cdesc []byte288if manifest == nil {289// legacy prebuild - resort to in-workspace content init290cdesc, err = executor.Prepare(&csapi.WorkspaceInitializer{Spec: &csapi.WorkspaceInitializer_Prebuild{Prebuild: pb}}, map[string]string{291pb.Prebuild.Snapshot: info.URL,292})293if err != nil {294return nil, nil, err295}296} else {297// fwb prebuild - add snapshot as content layer298var ls []Layer299ls, err = s.layerFromContentManifest(ctx, manifest, csapi.WorkspaceInitFromPrebuild, false)300if err != nil {301return nil, nil, err302}303l = append(l, ls...)304305// and run no-snapshot prebuild init in workspace306cdesc, err = executor.Prepare(&csapi.WorkspaceInitializer{307Spec: &csapi.WorkspaceInitializer_Prebuild{308Prebuild: &csapi.PrebuildInitializer{309Git: pb.Git,310},311},312}, nil)313if err != nil {314return nil, nil, err315}316}317318layer, err := contentDescriptorToLayer(cdesc)319320if err != nil {321return nil, nil, err322}323l = append(l, *layer)324return l, manifest, nil325}326327func (s *Provider) layerFromContentManifest(ctx context.Context, mf *csapi.WorkspaceContentManifest, initsrc csapi.WorkspaceInitSource, ready bool) (l []Layer, err error) {328// we have a valid full workspace backup329l = make([]Layer, len(mf.Layers))330for i, mfl := range mf.Layers {331info, err := s.Storage.SignDownload(ctx, mfl.Bucket, mfl.Object, &storage.SignedURLOptions{})332if err != nil {333return nil, err334}335if info.Meta.Digest != mfl.Digest.String() {336return nil, xerrors.Errorf("digest mismatch for %s/%s: expected %s, got %s", mfl.Bucket, mfl.Object, mfl.Digest, info.Meta.Digest)337}338l[i] = Layer{339DiffID: mfl.DiffID.String(),340Digest: mfl.Digest.String(),341MediaType: mfl.MediaType,342URL: info.URL,343Size: mfl.Size,344}345}346347if ready {348rl, err := workspaceReadyLayer(initsrc)349if err != nil {350return nil, err351}352l = append(l, *rl)353}354return l, nil355}356357func contentDescriptorToLayer(cdesc []byte) (*Layer, error) {358return layerFromContent(359fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/workspace", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},360fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/workspace/.gitpod", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},361fileInLayer{&tar.Header{Typeflag: tar.TypeReg, Name: "/workspace/.gitpod/content.json", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755, Size: int64(len(cdesc))}, cdesc},362)363}364365func workspaceReadyLayer(src csapi.WorkspaceInitSource) (*Layer, error) {366msg := csapi.WorkspaceReadyMessage{367Source: src,368}369ctnt, err := json.Marshal(msg)370if err != nil {371return nil, err372}373374return layerFromContent(375fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/workspace", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},376fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/workspace/.gitpod", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},377fileInLayer{&tar.Header{Typeflag: tar.TypeReg, Name: "/workspace/.gitpod/ready", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755, Size: int64(len(ctnt))}, []byte(ctnt)},378)379}380381type fileInLayer struct {382Header *tar.Header383Content []byte384}385386func layerFromContent(fs ...fileInLayer) (*Layer, error) {387buf := bytes.NewBuffer(nil)388tw := tar.NewWriter(buf)389for _, h := range fs {390err := tw.WriteHeader(h.Header)391if err != nil {392return nil, xerrors.Errorf("cannot prepare content layer: %w", err)393}394395if len(h.Content) == 0 {396continue397}398_, err = tw.Write(h.Content)399if err != nil {400return nil, xerrors.Errorf("cannot prepare content layer: %w", err)401}402}403tw.Close()404405return &Layer{406Digest: digest.FromBytes(buf.Bytes()).String(),407Content: buf.Bytes(),408}, nil409}410411// Layer is a content layer which is meant to be added to a workspace's image412type Layer struct {413Content []byte414415URL string416Digest string417DiffID string418MediaType string419Size int64420}421422423