Path: blob/main/components/ws-daemon/pkg/internal/session/workspace.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 session56import (7"context"8"encoding/json"9"errors"10"fmt"11"io/fs"12"os"13"path/filepath"14"time"1516"github.com/opentracing/opentracing-go"17"github.com/sirupsen/logrus"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/pkg/git"24)2526const (27// AttrRemoteStorage is the name of the remote storage associated with a workspace.28// Expect this to be an instance of storage.RemoteStorage29AttrRemoteStorage = "remote-storage"3031// AttrWorkspaceServer is the name of the workspace server cancel func.32// Expect this to be an instance of context.CancelFunc33AttrWorkspaceServer = "workspace-server"3435// AttrWaitForContent is the name of the wait-for-content probe cancel func.36// Expect this to be an instance of context.CancelFunc37AttrWaitForContent = "wait-for-content"38)3940const (41// maxPendingChanges is the limit beyond which we no longer report pending changes.42// For example, if a workspace has then 150 untracked files, we'll report the first43// 100 followed by "... and 50 more".44//45// We do this to keep the load on our infrastructure light and because beyond this number46// the changes are irrelevant anyways.47maxPendingChanges = 10048)4950// Workspace is a single workspace on-disk that we're managing.51type Workspace struct {52// Location is the absolute path in the local filesystem where to find this workspace53Location string `json:"location"`54// CheckoutLocation is the path relative to location where the main Git working copy of this55// workspace resides. If this workspace has no Git working copy, this field is an empty string.56CheckoutLocation string `json:"checkoutLocation"`5758CreatedAt time.Time `json:"createdAt"`59DoBackup bool `json:"doBackup"`60// DoWipe is a mode that a) does not make backups and b) ensures leaving a clean slate on workspace stop61DoWipe bool `json:"doWipe"`62Owner string `json:"owner"`63WorkspaceID string `json:"metaID"`64InstanceID string `json:"workspaceID"`65LastGitStatus *csapi.GitStatus `json:"lastGitStatus"`66ContentManifest []byte `json:"contentManifest"`6768ServiceLocNode string `json:"serviceLocNode"`69ServiceLocDaemon string `json:"serviceLocDaemon"`7071RemoteStorageDisabled bool `json:"remoteStorageDisabled,omitempty"`72StorageQuota int `json:"storageQuota,omitempty"`7374XFSProjectID int `json:"xfsProjectID"`7576NonPersistentAttrs map[string]interface{} `json:"-"`77}7879// OWI produces the owner, workspace, instance log metadata from the information80// of this workspace.81func (s *Workspace) OWI() logrus.Fields {82return log.OWI(s.Owner, s.WorkspaceID, s.InstanceID)83}8485// WorkspaceState is the lifecycle state of a workspace86type WorkspaceState string8788const (89// WorkspaceInitializing means the workspace content is currently being initialized90WorkspaceInitializing WorkspaceState = "initializing"91// WorkspaceReady means the workspace content is available on disk92WorkspaceReady WorkspaceState = "ready"93// WorkspaceDisposing means the workspace content is currently being backed up and removed from disk.94// No workspace content modifications must take place anymore.95WorkspaceDisposing WorkspaceState = "disposing"96// WorkspaceDisposed means the workspace content has been backed up and will be removed from disk soon.97WorkspaceDisposed WorkspaceState = "disposed"98)99100// WorkspaceLivecycleHook can modify a workspace's non-persistent state.101// They're intended to start regular operations or initialize non-persistent objects.102type WorkspaceLivecycleHook func(ctx context.Context, ws *Workspace) error103104// Dispose marks the workspace as disposed and clears it from disk105func (s *Workspace) Dispose(ctx context.Context, hooks []WorkspaceLivecycleHook) (err error) {106//nolint:ineffassign,staticcheck107span, ctx := opentracing.StartSpanFromContext(ctx, "workspace.Dispose")108defer tracing.FinishSpan(span, &err)109110// we remove the workspace file first, so that should something go wrong while deleting the111// old workspace content we can garbage collect that content later.112err = os.Remove(s.persistentStateLocation())113if err != nil {114if errors.Is(err, fs.ErrNotExist) {115log.WithError(err).WithFields(s.OWI()).Warn("workspace persistent state location not exist")116err = nil117} else {118return xerrors.Errorf("cannot remove workspace persistent state location: %w", err)119}120}121122for _, h := range hooks {123err := h(ctx, s)124if err != nil {125return err126}127}128129err = os.RemoveAll(s.Location)130if err != nil {131return xerrors.Errorf("cannot remove workspace all: %w", err)132}133134return nil135}136137// UpdateGitStatus attempts to update the LastGitStatus from the workspace's local working copy.138func (s *Workspace) UpdateGitStatus(ctx context.Context) (res *csapi.GitStatus, err error) {139var loc string140141loc = s.Location142if loc == "" {143log.WithField("loc", loc).WithFields(s.OWI()).Debug("not updating Git status of FWB workspace")144return145}146147loc = filepath.Join(loc, s.CheckoutLocation)148if !git.IsWorkingCopy(loc) {149log.WithField("loc", loc).WithField("checkout location", s.CheckoutLocation).WithFields(s.OWI()).Debug("did not find a Git working copy - not updating Git status")150return nil, nil151}152153c := git.Client{Location: loc}154155stat, err := c.Status(ctx)156if err != nil {157return nil, err158}159160s.LastGitStatus = toGitStatus(stat)161162err = s.Persist()163if err != nil {164log.WithError(err).WithFields(s.OWI()).Warn("cannot persist latest Git status")165}166167return s.LastGitStatus, nil168}169170func toGitStatus(s *git.Status) *csapi.GitStatus {171limit := func(entries []string) []string {172if len(entries) > maxPendingChanges {173return append(entries[0:maxPendingChanges], fmt.Sprintf("... and %d more", len(entries)-maxPendingChanges))174}175176return entries177}178179return &csapi.GitStatus{180Branch: s.BranchHead,181LatestCommit: s.LatestCommit,182UncommitedFiles: limit(s.UncommitedFiles),183TotalUncommitedFiles: int64(len(s.UncommitedFiles)),184UntrackedFiles: limit(s.UntrackedFiles),185TotalUntrackedFiles: int64(len(s.UntrackedFiles)),186UnpushedCommits: limit(s.UnpushedCommits),187TotalUnpushedCommits: int64(len(s.UnpushedCommits)),188}189}190191func (s *Workspace) persistentStateLocation() string {192return filepath.Join(filepath.Dir(s.Location), fmt.Sprintf("%s.workspace.json", s.InstanceID))193}194195func (s *Workspace) Persist() error {196fc, err := json.Marshal(s)197if err != nil {198return xerrors.Errorf("cannot marshal workspace: %w", err)199}200201err = os.WriteFile(s.persistentStateLocation(), fc, 0644)202if err != nil {203return xerrors.Errorf("cannot persist workspace: %w", err)204}205206return nil207}208209func LoadWorkspace(ctx context.Context, path string) (sess *Workspace, err error) {210span, _ := opentracing.StartSpanFromContext(ctx, "loadWorkspace")211defer tracing.FinishSpan(span, &err)212213fc, err := os.ReadFile(path)214if err != nil {215return nil, fmt.Errorf("cannot load session file: %w", err)216}217218var workspace Workspace219err = json.Unmarshal(fc, &workspace)220if err != nil {221return nil, fmt.Errorf("cannot unmarshal session file: %w", err)222}223224workspace.NonPersistentAttrs = make(map[string]interface{})225return &workspace, nil226}227228229