Skip to content

Add persistent volume support for workspaces #9242

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions components/content-service/pkg/layer/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,105 @@ func (s *Provider) GetContentLayer(ctx context.Context, owner, workspaceID strin
return nil, nil, xerrors.Errorf("no backup or valid initializer present")
}

// GetContentLayerPVC provides the content layer for a workspace that uses PVC feature
func (s *Provider) GetContentLayerPVC(ctx context.Context, owner, workspaceID string, initializer *csapi.WorkspaceInitializer) (l []Layer, manifest *csapi.WorkspaceContentManifest, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: we could refactor these functions GetContentLayer and GetContentLayerPVC into one in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Once PVC is stable and we switched over to it, old code will be removed. (so that we will have only one version of that function)

span, ctx := tracing.FromContext(ctx, "GetContentLayer")
defer tracing.FinishSpan(span, &err)
tracing.ApplyOWI(span, log.OWI(owner, workspaceID, ""))

defer func() {
// we never return a nil manifest, just maybe an empty one
if manifest == nil {
manifest = &csapi.WorkspaceContentManifest{
Type: csapi.TypeFullWorkspaceContentV1,
}
}
}()

// check if workspace has an FWB
var (
bucket = s.Storage.Bucket(owner)
)
span.LogKV("bucket", bucket)

// check if legacy workspace backup is present
var layer *Layer
info, err := s.Storage.SignDownload(ctx, bucket, fmt.Sprintf(fmtLegacyBackupName, workspaceID), &storage.SignedURLOptions{})
if err != nil && !xerrors.Is(err, storage.ErrNotFound) {
return nil, nil, err
}
if err == nil {
span.LogKV("backup found", "legacy workspace backup")

cdesc, err := executor.PrepareFromBackup(info.URL)
if err != nil {
return nil, nil, err
}

layer, err = contentDescriptorToLayerPVC(cdesc)
if err != nil {
return nil, nil, err
}

l = []Layer{*layer}
return l, manifest, nil
}

// At this point we've found neither a full-workspace-backup, nor a legacy backup.
// It's time to use the initializer.
if gis := initializer.GetSnapshot(); gis != nil {
return s.getSnapshotContentLayer(ctx, gis)
}
if pis := initializer.GetPrebuild(); pis != nil {
l, manifest, err = s.getPrebuildContentLayer(ctx, pis)
if err != nil {
log.WithError(err).WithFields(log.OWI(owner, workspaceID, "")).Warn("cannot initialize from prebuild - falling back to Git")
span.LogKV("fallback-to-git", err.Error())

// we failed creating a prebuild initializer, so let's try falling back to the Git part.
var init []*csapi.WorkspaceInitializer
for _, gi := range pis.Git {
init = append(init, &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Git{
Git: gi,
},
})
}
initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Composite{
Composite: &csapi.CompositeInitializer{
Initializer: init,
},
},
}
} else {
// creating the initializer worked - we're done here
return
}
}
if gis := initializer.GetGit(); gis != nil {
span.LogKV("initializer", "Git")

cdesc, err := executor.Prepare(initializer, nil)
if err != nil {
return nil, nil, err
}

layer, err = contentDescriptorToLayerPVC(cdesc)
if err != nil {
return nil, nil, err
}
return []Layer{*layer}, nil, nil
}
if initializer.GetBackup() != nil {
// We were asked to restore a backup and have tried above. We've failed to restore the backup,
// hance the backup initializer failed.
return nil, nil, xerrors.Errorf("no backup found")
}

return nil, nil, xerrors.Errorf("no backup or valid initializer present")
}

func (s *Provider) getSnapshotContentLayer(ctx context.Context, sp *csapi.SnapshotInitializer) (l []Layer, manifest *csapi.WorkspaceContentManifest, err error) {
span, ctx := tracing.FromContext(ctx, "getSnapshotContentLayer")
defer tracing.FinishSpan(span, &err)
Expand Down Expand Up @@ -368,6 +467,17 @@ func contentDescriptorToLayer(cdesc []byte) (*Layer, error) {
)
}

// version of this function for persistent volume claim feature
// we cannot use /workspace folder as when mounting /workspace folder through PVC
// it will mask anything that was in container layer, hence we are using /.workspace instead here
func contentDescriptorToLayerPVC(cdesc []byte) (*Layer, error) {
return layerFromContent(
fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/.workspace", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},
fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/.workspace/.gitpod", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},
fileInLayer{&tar.Header{Typeflag: tar.TypeReg, Name: "/.workspace/.gitpod/content.json", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755, Size: int64(len(cdesc))}, cdesc},
)
}

func workspaceReadyLayer(src csapi.WorkspaceInitSource) (*Layer, error) {
msg := csapi.WorkspaceReadyMessage{
Source: src,
Expand Down
8 changes: 7 additions & 1 deletion components/supervisor/pkg/supervisor/supervisor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,12 @@ func startContentInit(ctx context.Context, cfg *Config, wg *sync.WaitGroup, cst
}()

fn := "/workspace/.gitpod/content.json"
fnReady := "/workspace/.gitpod/ready"
if _, err := os.Stat("/.workspace/.gitpod/content.json"); !os.IsNotExist(err) {
fn = "/.workspace/.gitpod/content.json"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make it explicit that these paths come from a PVC mount, why not make it refer to a variable shared with the following location? This would surely help the code reader.
https://github.com/gitpod-io/gitpod/pull/9242/files#diff-46a025104f556cc7faed4319cb1c9d2bd0b5097231b42707825aaf0df24d7337R477

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will be switching default to always use /.workspace folder, so that we don't need to do all those checks in the future

fnReady = "/.workspace/.gitpod/ready"
log.Info("Detected content.json in /.workspace folder, assuming PVC feature enabled")
}
f, err := os.Open(fn)
if os.IsNotExist(err) {
log.WithError(err).Info("no content init descriptor found - not trying to run it")
Expand All @@ -1230,7 +1236,7 @@ func startContentInit(ctx context.Context, cfg *Config, wg *sync.WaitGroup, cst
// TODO: rewrite using fsnotify
t := time.NewTicker(100 * time.Millisecond)
for range t.C {
b, err := os.ReadFile("/workspace/.gitpod/ready")
b, err := os.ReadFile(fnReady)
if err != nil {
if !os.IsNotExist(err) {
log.WithError(err).Error("cannot read content ready file")
Expand Down
6 changes: 4 additions & 2 deletions components/workspacekit/cmd/rings.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ var ring0Cmd = &cobra.Command{
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(),
"WORKSPACEKIT_FSSHIFT="+prep.FsShift.String(),
fmt.Sprintf("WORKSPACEKIT_FULL_WORKSPACE_BACKUP=%v", prep.FullWorkspaceBackup),
fmt.Sprintf("WORKSPACEKIT_NO_WORKSPACE_MOUNT=%v", prep.FullWorkspaceBackup || prep.PersistentVolumeClaim),
)

if err := cmd.Start(); err != nil {
Expand Down Expand Up @@ -303,7 +303,8 @@ var ring1Cmd = &cobra.Command{

// FWB workspaces do not require mounting /workspace
// if that is done, the backup will not contain any change in the directory
if os.Getenv("WORKSPACEKIT_FULL_WORKSPACE_BACKUP") != "true" {
// same applies to persistent volume claims, we cannot mount /workspace folder when PVC is used
if os.Getenv("WORKSPACEKIT_NO_WORKSPACE_MOUNT") != "true" {
mnts = append(mnts,
mnte{Target: "/workspace", Flags: unix.MS_BIND | unix.MS_REC},
)
Expand Down Expand Up @@ -416,6 +417,7 @@ var ring1Cmd = &cobra.Command{
log.WithError(err).Error("cannot mount proc")
return
}

_, err = client.EvacuateCGroup(ctx, &daemonapi.EvacuateCGroupRequest{})
if err != nil {
client.Close()
Expand Down
5 changes: 5 additions & 0 deletions components/ws-daemon-api/daemon.proto
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ message InitWorkspaceRequest {

// storage_quota_bytes enforces a storage quate for the workspace if set to a value != 0
int64 storage_quota_bytes = 8;

// persistent_volume_claim means that we use PVC instead of HostPath to mount /workspace folder and content-init
// happens inside workspacekit instead of in ws-daemon. We also use k8s Snapshots to store\restore workspace content
// instead of GCS\tar.
bool persistent_volume_claim = 9;
Copy link
Contributor

@utam0k utam0k May 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using enumerations instead of using bool? e.g. VolumeType, StorageType etc
https://developers.google.com/protocol-buffers/docs/proto3#enum

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is applicable here, since this bool flag is used as feature flag. Once PVC feature is stable and working, we will remove old code and remove this feature flag and this new code will become default code path.

}

// WorkspaceMetadata is data associated with a workspace that's required for other parts of the system to function
Expand Down
2 changes: 2 additions & 0 deletions components/ws-daemon-api/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/gitpod-io/generated_code_dependencies

go 1.18

require google.golang.org/protobuf v1.28.0 // indirect
6 changes: 6 additions & 0 deletions components/ws-daemon-api/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
Loading