2023-06-22 21:44:12 +08:00

167 lines
5.7 KiB
Go

// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.
package content
import (
"context"
"errors"
"io/fs"
"os"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/common-go/tracing"
"github.com/gitpod-io/gitpod/content-service/pkg/initializer"
"github.com/gitpod-io/gitpod/content-service/pkg/storage"
"github.com/gitpod-io/gitpod/ws-daemon/api"
"github.com/gitpod-io/gitpod/ws-daemon/pkg/internal/session"
"github.com/gitpod-io/gitpod/ws-daemon/pkg/iws"
"github.com/gitpod-io/gitpod/ws-daemon/pkg/quota"
"github.com/opentracing/opentracing-go"
"golang.org/x/xerrors"
)
// WorkspaceLifecycleHooks configures the lifecycle hooks for all workspaces
func WorkspaceLifecycleHooks(cfg Config, workspaceCIDR string, uidmapper *iws.Uidmapper, xfs *quota.XFS, cgroupMountPoint string) map[session.WorkspaceState][]session.WorkspaceLivecycleHook {
// startIWS starts the in-workspace service for a workspace. This lifecycle hook is idempotent, hence can - and must -
// be called on initialization and ready. The on-ready hook exists only to support ws-daemon restarts.
startIWS := iws.ServeWorkspace(uidmapper, api.FSShiftMethod(cfg.UserNamespaces.FSShift), cgroupMountPoint, workspaceCIDR)
return map[session.WorkspaceState][]session.WorkspaceLivecycleHook{
session.WorkspaceInitializing: {
hookSetupWorkspaceLocation,
startIWS, // workspacekit is waiting for starting IWS, so it needs to start as soon as possible.
hookSetupRemoteStorage(cfg),
// When starting a workspace, use soft limit for the following reason to ensure content is restored
// - workspacekit needs to generate some temporary file when starting a workspace
// - when extracting tar file, tar command create some symlinks following a original content
hookInstallQuota(xfs, false),
},
session.WorkspaceReady: {
startIWS,
hookSetupRemoteStorage(cfg),
hookInstallQuota(xfs, true),
},
session.WorkspaceDisposed: {
iws.StopServingWorkspace,
hookRemoveQuota(xfs),
},
}
}
// hookSetupRemoteStorage configures the remote storage for a workspace
func hookSetupRemoteStorage(cfg Config) session.WorkspaceLivecycleHook {
return func(ctx context.Context, ws *session.Workspace) (err error) {
span, ctx := opentracing.StartSpanFromContext(ctx, "hook.SetupRemoteStorage")
defer tracing.FinishSpan(span, &err)
if _, ok := ws.NonPersistentAttrs[session.AttrRemoteStorage]; !ws.RemoteStorageDisabled && !ok {
remoteStorage, err := storage.NewDirectAccess(&cfg.Storage)
if err != nil {
return xerrors.Errorf("cannot use configured storage: %w", err)
}
err = remoteStorage.Init(ctx, ws.Owner, ws.WorkspaceID, ws.InstanceID)
if err != nil {
return xerrors.Errorf("cannot use configured storage: %w", err)
}
err = remoteStorage.EnsureExists(ctx)
if err != nil {
return xerrors.Errorf("cannot use configured storage: %w", err)
}
ws.NonPersistentAttrs[session.AttrRemoteStorage] = remoteStorage
}
return nil
}
}
// hookSetupWorkspaceLocation recreates the workspace location
func hookSetupWorkspaceLocation(ctx context.Context, ws *session.Workspace) (err error) {
//nolint:ineffassign
span, _ := opentracing.StartSpanFromContext(ctx, "hook.SetupWorkspaceLocation")
defer tracing.FinishSpan(span, &err)
location := ws.Location
// 1. Clean out the workspace directory
if _, err := os.Stat(location); errors.Is(err, fs.ErrNotExist) {
// in the very unlikely event that the workspace Pod did not mount (and thus create) the workspace directory, create it
err = os.Mkdir(location, 0755)
if os.IsExist(err) {
log.WithError(err).WithFields(ws.OWI()).WithField("location", location).Debug("ran into non-atomic workspace location existence check")
} else if err != nil {
return xerrors.Errorf("cannot create workspace: %w", err)
}
}
// Chown the workspace directory
err = os.Chown(location, initializer.GitpodUID, initializer.GitpodGID)
if err != nil {
return xerrors.Errorf("cannot create workspace: %w", err)
}
return nil
}
// hookInstallQuota enforces filesystem quota on the workspace location (if the filesystem supports it)
func hookInstallQuota(xfs *quota.XFS, isHard bool) session.WorkspaceLivecycleHook {
return func(ctx context.Context, ws *session.Workspace) (err error) {
span, _ := opentracing.StartSpanFromContext(ctx, "hook.InstallQuota")
defer tracing.FinishSpan(span, &err)
if xfs == nil {
log.WithFields(ws.OWI()).Warn("no xfs definition")
return nil
}
if ws.StorageQuota == 0 {
log.WithFields(ws.OWI()).Warn("no storage quota defined")
return nil
}
size := quota.Size(ws.StorageQuota)
log.WithFields(ws.OWI()).WithField("isHard", isHard).WithField("size", size).WithField("directory", ws.Location).Debug("setting disk quota")
var (
prj int
)
if ws.XFSProjectID != 0 {
xfs.RegisterProject(ws.XFSProjectID)
prj, err = xfs.SetQuotaWithPrjId(ws.Location, size, ws.XFSProjectID, isHard)
} else {
prj, err = xfs.SetQuota(ws.Location, size, isHard)
}
if err != nil {
log.WithFields(ws.OWI()).WithError(err).Warn("cannot enforce workspace size limit")
}
ws.XFSProjectID = int(prj)
return nil
}
}
// hookRemoveQuota removes the filesystem quota, freeing up resources if need be
func hookRemoveQuota(xfs *quota.XFS) session.WorkspaceLivecycleHook {
return func(ctx context.Context, ws *session.Workspace) (err error) {
span, _ := opentracing.StartSpanFromContext(ctx, "hook.RemoveQuota")
defer tracing.FinishSpan(span, &err)
if xfs == nil {
return nil
}
if xfs == nil {
return nil
}
if ws.XFSProjectID == 0 {
return nil
}
return xfs.RemoveQuota(ws.XFSProjectID)
}
}