Path: blob/main/components/ws-daemon/pkg/content/hooks.go
2500 views
// Copyright (c) 2021 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 content56import (7"context"8"errors"9"io/fs"10"os"11"path/filepath"1213"github.com/gitpod-io/gitpod/common-go/log"14"github.com/gitpod-io/gitpod/common-go/tracing"15"github.com/gitpod-io/gitpod/content-service/pkg/initializer"16"github.com/gitpod-io/gitpod/content-service/pkg/storage"17"github.com/gitpod-io/gitpod/ws-daemon/api"18daemonapi "github.com/gitpod-io/gitpod/ws-daemon/api"19"github.com/gitpod-io/gitpod/ws-daemon/pkg/internal/session"20"github.com/gitpod-io/gitpod/ws-daemon/pkg/iws"21"github.com/gitpod-io/gitpod/ws-daemon/pkg/quota"22"github.com/opentracing/opentracing-go"23"golang.org/x/xerrors"24"google.golang.org/grpc"25"google.golang.org/grpc/credentials/insecure"26)2728// WorkspaceLifecycleHooks configures the lifecycle hooks for all workspaces29func WorkspaceLifecycleHooks(cfg Config, workspaceCIDR string, uidmapper *iws.Uidmapper, xfs *quota.XFS, cgroupMountPoint string) map[session.WorkspaceState][]session.WorkspaceLivecycleHook {30// startIWS starts the in-workspace service for a workspace. This lifecycle hook is idempotent, hence can - and must -31// be called on initialization and ready. The on-ready hook exists only to support ws-daemon restarts.32startIWS := iws.ServeWorkspace(uidmapper, api.FSShiftMethod(cfg.UserNamespaces.FSShift), cgroupMountPoint, workspaceCIDR)3334return map[session.WorkspaceState][]session.WorkspaceLivecycleHook{35session.WorkspaceInitializing: {36hookSetupWorkspaceLocation,37startIWS, // workspacekit is waiting for starting IWS, so it needs to start as soon as possible.38hookSetupRemoteStorage(cfg),39// When starting a workspace, use soft limit for the following reason to ensure content is restored40// - workspacekit needs to generate some temporary file when starting a workspace41// - when extracting tar file, tar command create some symlinks following a original content42hookInstallQuota(xfs, false),43},44session.WorkspaceReady: {45startIWS,46hookSetupRemoteStorage(cfg),47hookInstallQuota(xfs, true),48},49session.WorkspaceDisposed: {50hookWipingTeardown(), // if ws.DoWipe == true: make sure we 100% tear down the workspace51iws.StopServingWorkspace,52hookRemoveQuota(xfs),53},54}55}5657// hookSetupRemoteStorage configures the remote storage for a workspace58func hookSetupRemoteStorage(cfg Config) session.WorkspaceLivecycleHook {59return func(ctx context.Context, ws *session.Workspace) (err error) {60span, ctx := opentracing.StartSpanFromContext(ctx, "hook.SetupRemoteStorage")61defer tracing.FinishSpan(span, &err)6263if _, ok := ws.NonPersistentAttrs[session.AttrRemoteStorage]; !ws.RemoteStorageDisabled && !ok {64remoteStorage, err := storage.NewDirectAccess(&cfg.Storage)65if err != nil {66return xerrors.Errorf("cannot use configured storage: %w", err)67}6869err = remoteStorage.Init(ctx, ws.Owner, ws.WorkspaceID, ws.InstanceID)70if err != nil {71return xerrors.Errorf("cannot use configured storage: %w", err)72}7374err = remoteStorage.EnsureExists(ctx)75if err != nil {76return xerrors.Errorf("cannot use configured storage: %w", err)77}7879ws.NonPersistentAttrs[session.AttrRemoteStorage] = remoteStorage80}8182return nil83}84}8586// hookSetupWorkspaceLocation recreates the workspace location87func hookSetupWorkspaceLocation(ctx context.Context, ws *session.Workspace) (err error) {88//nolint:ineffassign89span, _ := opentracing.StartSpanFromContext(ctx, "hook.SetupWorkspaceLocation")90defer tracing.FinishSpan(span, &err)91location := ws.Location9293// 1. Clean out the workspace directory94if _, err := os.Stat(location); errors.Is(err, fs.ErrNotExist) {95// in the very unlikely event that the workspace Pod did not mount (and thus create) the workspace directory, create it96err = os.Mkdir(location, 0755)97if os.IsExist(err) {98log.WithError(err).WithFields(ws.OWI()).WithField("location", location).Debug("ran into non-atomic workspace location existence check")99} else if err != nil {100return xerrors.Errorf("cannot create workspace: %w", err)101}102}103104// Chown the workspace directory105err = os.Chown(location, initializer.GitpodUID, initializer.GitpodGID)106if err != nil {107return xerrors.Errorf("cannot create workspace: %w", err)108}109return nil110}111112// hookInstallQuota enforces filesystem quota on the workspace location (if the filesystem supports it)113func hookInstallQuota(xfs *quota.XFS, isHard bool) session.WorkspaceLivecycleHook {114return func(ctx context.Context, ws *session.Workspace) (err error) {115span, _ := opentracing.StartSpanFromContext(ctx, "hook.InstallQuota")116defer tracing.FinishSpan(span, &err)117118if xfs == nil {119log.WithFields(ws.OWI()).Warn("no xfs definition")120return nil121}122123if ws.StorageQuota == 0 {124log.WithFields(ws.OWI()).Warn("no storage quota defined")125return nil126}127128size := quota.Size(ws.StorageQuota)129130log.WithFields(ws.OWI()).WithField("isHard", isHard).WithField("size", size).WithField("directory", ws.Location).Debug("setting disk quota")131132var (133prj int134)135if ws.XFSProjectID != 0 {136xfs.RegisterProject(ws.XFSProjectID)137prj, err = xfs.SetQuotaWithPrjId(ws.Location, size, ws.XFSProjectID, isHard)138} else {139prj, err = xfs.SetQuota(ws.Location, size, isHard)140}141142if err != nil {143log.WithFields(ws.OWI()).WithError(err).Warn("cannot enforce workspace size limit")144}145ws.XFSProjectID = int(prj)146147return nil148}149}150151// hookRemoveQuota removes the filesystem quota, freeing up resources if need be152func hookRemoveQuota(xfs *quota.XFS) session.WorkspaceLivecycleHook {153return func(ctx context.Context, ws *session.Workspace) (err error) {154span, _ := opentracing.StartSpanFromContext(ctx, "hook.RemoveQuota")155defer tracing.FinishSpan(span, &err)156157if xfs == nil {158return nil159}160161if xfs == nil {162return nil163}164if ws.XFSProjectID == 0 {165return nil166}167168return xfs.RemoveQuota(ws.XFSProjectID)169}170}171172func hookWipingTeardown() session.WorkspaceLivecycleHook {173return func(ctx context.Context, ws *session.Workspace) error {174log := log.WithFields(ws.OWI())175176if !ws.DoWipe {177// this is the "default" case for 99% of all workspaces178// TODO(gpl): We should probably make this the default for all workspaces - but not with this PR179return nil180}181182socketFN := filepath.Join(ws.ServiceLocDaemon, "daemon.sock")183conn, err := grpc.DialContext(ctx, "unix://"+socketFN, grpc.WithTransportCredentials(insecure.NewCredentials()))184if err != nil {185log.WithError(err).Error("error connecting to IWS for WipingTeardown")186return nil187}188client := daemonapi.NewInWorkspaceServiceClient(conn)189190res, err := client.WipingTeardown(ctx, &daemonapi.WipingTeardownRequest{191DoWipe: ws.DoWipe,192})193if err != nil {194return err195}196log.WithField("success", res.Success).Debug("wiping teardown done")197198return nil199}200}201202203