mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
388 lines
12 KiB
Go
388 lines
12 KiB
Go
// Copyright (c) 2020 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 session
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/opentracing/opentracing-go"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/gitpod-io/gitpod/common-go/log"
|
|
"github.com/gitpod-io/gitpod/common-go/tracing"
|
|
csapi "github.com/gitpod-io/gitpod/content-service/api"
|
|
"github.com/gitpod-io/gitpod/content-service/pkg/git"
|
|
)
|
|
|
|
const (
|
|
// AttrRemoteStorage is the name of the remote storage associated with a workspace.
|
|
// Expect this to be an instance of storage.RemoteStorage
|
|
AttrRemoteStorage = "remote-storage"
|
|
|
|
// AttrWorkspaceServer is the name of the workspace server cancel func.
|
|
// Expect this to be an instance of context.CancelFunc
|
|
AttrWorkspaceServer = "workspace-server"
|
|
|
|
// AttrWaitForContent is the name of the wait-for-content probe cancel func.
|
|
// Expect this to be an instance of context.CancelFunc
|
|
AttrWaitForContent = "wait-for-content"
|
|
)
|
|
|
|
const (
|
|
// maxPendingChanges is the limit beyond which we no longer report pending changes.
|
|
// For example, if a workspace has then 150 untracked files, we'll report the first
|
|
// 100 followed by "... and 50 more".
|
|
//
|
|
// We do this to keep the load on our infrastructure light and because beyond this number
|
|
// the changes are irrelevant anyways.
|
|
maxPendingChanges = 100
|
|
)
|
|
|
|
// Workspace is a single workspace on-disk that we're managing.
|
|
type Workspace struct {
|
|
// Location is the absolute path in the local filesystem where to find this workspace
|
|
Location string `json:"location"`
|
|
// CheckoutLocation is the path relative to location where the main Git working copy of this
|
|
// workspace resides. If this workspace has no Git working copy, this field is an empty string.
|
|
CheckoutLocation string `json:"checkoutLocation"`
|
|
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
DoBackup bool `json:"doBackup"`
|
|
Owner string `json:"owner"`
|
|
WorkspaceID string `json:"metaID"`
|
|
InstanceID string `json:"workspaceID"`
|
|
LastGitStatus *csapi.GitStatus `json:"lastGitStatus"`
|
|
FullWorkspaceBackup bool `json:"fullWorkspaceBackup"`
|
|
PersistentVolumeClaim bool `json:"persistentVolumeClaim"`
|
|
ContentManifest []byte `json:"contentManifest"`
|
|
|
|
ServiceLocNode string `json:"serviceLocNode"`
|
|
ServiceLocDaemon string `json:"serviceLocDaemon"`
|
|
|
|
RemoteStorageDisabled bool `json:"remoteStorageDisabled,omitempty"`
|
|
StorageQuota int `json:"storageQuota,omitempty"`
|
|
|
|
XFSProjectID int `json:"xfsProjectID"`
|
|
|
|
NonPersistentAttrs map[string]interface{} `json:"-"`
|
|
|
|
store *Store
|
|
state WorkspaceState
|
|
stateLock sync.RWMutex
|
|
operatingCondition *sync.Cond
|
|
}
|
|
|
|
// OWI produces the owner, workspace, instance log metadata from the information
|
|
// of this workspace.
|
|
func (s *Workspace) OWI() logrus.Fields {
|
|
return log.OWI(s.Owner, s.WorkspaceID, s.InstanceID)
|
|
}
|
|
|
|
// WorkspaceState is the lifecycle state of a workspace
|
|
type WorkspaceState string
|
|
|
|
const (
|
|
// WorkspaceInitializing means the workspace content is is currently being initialized
|
|
WorkspaceInitializing WorkspaceState = "initializing"
|
|
// WorkspaceReady means the workspace content is available on disk
|
|
WorkspaceReady WorkspaceState = "ready"
|
|
// WorkspaceDisposing means the workspace content is currently being backed up and removed from disk.
|
|
// No workspace content modifications must take place anymore.
|
|
WorkspaceDisposing WorkspaceState = "disposing"
|
|
// WorkspaceDisposed means the workspace content has been backed up and will be removed from disk soon.
|
|
WorkspaceDisposed WorkspaceState = "disposed"
|
|
)
|
|
|
|
// WaitForInit waits until this workspace is initialized
|
|
func (s *Workspace) WaitForInit(ctx context.Context) (ready bool) {
|
|
//nolint:ineffassign,staticcheck
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "workspace.WaitForInit")
|
|
defer tracing.FinishSpan(span, nil)
|
|
|
|
s.stateLock.RLock()
|
|
if s.state == WorkspaceReady {
|
|
s.stateLock.RUnlock()
|
|
return true
|
|
} else if s.state != WorkspaceInitializing {
|
|
s.stateLock.RUnlock()
|
|
return false
|
|
}
|
|
s.stateLock.RUnlock()
|
|
|
|
s.operatingCondition.L.Lock()
|
|
s.operatingCondition.Wait()
|
|
// make sure that state is indeed ready when done waiting
|
|
ready = s.state == WorkspaceReady
|
|
s.operatingCondition.L.Unlock()
|
|
return
|
|
}
|
|
|
|
// MarkInitDone marks this workspace as initialized and writes this workspace to disk so that it can be restored should ws-daemon crash/be restarted
|
|
func (s *Workspace) MarkInitDone(ctx context.Context) (err error) {
|
|
//nolint:ineffassign,staticcheck
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "workspace.MarkInitDone")
|
|
defer tracing.FinishSpan(span, &err)
|
|
|
|
// We persist before changing state so that we only mark everything as ready
|
|
// if we actually have a persistent workspace. Otherwise we might have wsman thinking
|
|
// something different than a restarted ws-daemon.
|
|
err = s.Persist()
|
|
if err != nil {
|
|
return xerrors.Errorf("cannot mark init done: %w", err)
|
|
}
|
|
|
|
s.stateLock.Lock()
|
|
s.state = WorkspaceReady
|
|
s.operatingCondition.Broadcast()
|
|
s.stateLock.Unlock()
|
|
|
|
err = s.store.runLifecycleHooks(ctx, s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Now that the rest of the world know's we're ready, we have to remember that ourselves.
|
|
err = s.Persist()
|
|
if err != nil {
|
|
return xerrors.Errorf("cannot mark init done: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WaitOrMarkForDisposal marks the workspace as disposing, or if it's already in that state waits until it's actually disposed
|
|
func (s *Workspace) WaitOrMarkForDisposal(ctx context.Context) (done bool, repo *csapi.GitStatus, err error) {
|
|
//nolint:ineffassign,staticcheck
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "workspace.WaitOrMarkForDisposal")
|
|
defer tracing.FinishSpan(span, &err)
|
|
|
|
s.stateLock.Lock()
|
|
if s.state == WorkspaceDisposed {
|
|
s.stateLock.Unlock()
|
|
return true, nil, nil
|
|
} else if s.state != WorkspaceDisposing {
|
|
s.state = WorkspaceDisposing
|
|
s.stateLock.Unlock()
|
|
|
|
err = s.Persist()
|
|
if err != nil {
|
|
return false, nil, xerrors.Errorf("cannot mark as disposing: %w", err)
|
|
}
|
|
|
|
err = s.store.runLifecycleHooks(ctx, s)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
return false, nil, nil
|
|
}
|
|
s.stateLock.Unlock()
|
|
|
|
s.operatingCondition.L.Lock()
|
|
s.operatingCondition.Wait()
|
|
done = true
|
|
repo = s.LastGitStatus
|
|
s.operatingCondition.L.Unlock()
|
|
return
|
|
}
|
|
|
|
// Dispose marks the workspace as disposed and clears it from disk
|
|
func (s *Workspace) Dispose(ctx context.Context) (err error) {
|
|
//nolint:ineffassign,staticcheck
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "workspace.Dispose")
|
|
defer tracing.FinishSpan(span, &err)
|
|
|
|
// we remove the workspace file first, so that should something go wrong while deleting the
|
|
// old workspace content we can garbage collect that content later.
|
|
err = os.Remove(s.persistentStateLocation())
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
log.WithError(err).Warn("workspace persistent state location not exist")
|
|
err = nil
|
|
} else {
|
|
return xerrors.Errorf("cannot remove workspace persistent state location: %w", err)
|
|
}
|
|
}
|
|
|
|
s.stateLock.Lock()
|
|
s.state = WorkspaceDisposed
|
|
s.operatingCondition.Broadcast()
|
|
s.stateLock.Unlock()
|
|
|
|
err = s.store.runLifecycleHooks(ctx, s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if s.PersistentVolumeClaim {
|
|
// nothing to dispose as files are on persistent volume claim
|
|
return nil
|
|
}
|
|
|
|
if !s.FullWorkspaceBackup {
|
|
err = os.RemoveAll(s.Location)
|
|
}
|
|
if err != nil {
|
|
return xerrors.Errorf("cannot remove workspace all: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsReady returns true if the workspace is in the ready state
|
|
func (s *Workspace) IsReady() bool {
|
|
s.stateLock.RLock()
|
|
r := s.state == WorkspaceReady
|
|
s.stateLock.RUnlock()
|
|
return r
|
|
}
|
|
|
|
// IsDisposing returns true if the workspace is in the disposing/disposed state
|
|
func (s *Workspace) IsDisposing() bool {
|
|
s.stateLock.RLock()
|
|
r := s.state == WorkspaceDisposing || s.state == WorkspaceDisposed
|
|
s.stateLock.RUnlock()
|
|
return r
|
|
}
|
|
|
|
// SetGitStatus sets the last git status field and persists the change
|
|
func (s *Workspace) SetGitStatus(status *csapi.GitStatus) error {
|
|
s.stateLock.Lock()
|
|
s.LastGitStatus = status
|
|
s.stateLock.Unlock()
|
|
|
|
return s.Persist()
|
|
}
|
|
|
|
// UpdateGitStatus attempts to update the LastGitStatus from the workspace's local working copy.
|
|
func (s *Workspace) UpdateGitStatus(ctx context.Context, persistentVolumeClaim bool) (res *csapi.GitStatus, err error) {
|
|
var loc string
|
|
if persistentVolumeClaim {
|
|
loc = filepath.Join(s.ServiceLocDaemon, "prestophookdata")
|
|
stat, err := git.GitStatusFromFiles(ctx, loc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.LastGitStatus = toGitStatus(stat)
|
|
} else {
|
|
loc = s.Location
|
|
if loc == "" {
|
|
// FWB workspaces don't have `Location` set, but rather ServiceLocDaemon and ServiceLocNode.
|
|
// We'd can't easily produce the Git status, because in this context `mark` isn't mounted, and `upper`
|
|
// only contains the full git working copy if the content was just initialised.
|
|
// Something like
|
|
// loc = filepath.Join(s.ServiceLocDaemon, "mark", "workspace")
|
|
// does not work.
|
|
//
|
|
// TODO(cw): figure out a way to get ahold of the Git status.
|
|
log.WithField("loc", loc).WithFields(s.OWI()).Debug("not updating Git status of FWB workspace")
|
|
return
|
|
}
|
|
|
|
loc = filepath.Join(loc, s.CheckoutLocation)
|
|
if !git.IsWorkingCopy(loc) {
|
|
log.WithField("loc", loc).WithField("checkout location", s.CheckoutLocation).WithFields(s.OWI()).Debug("did not find a Git working copy - not updating Git status")
|
|
return nil, nil
|
|
}
|
|
|
|
c := git.Client{Location: loc}
|
|
|
|
stat, err := c.Status(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.LastGitStatus = toGitStatus(stat)
|
|
}
|
|
|
|
err = s.Persist()
|
|
if err != nil {
|
|
log.WithError(err).WithFields(s.OWI()).Warn("cannot persist latest Git status")
|
|
err = nil
|
|
}
|
|
|
|
return s.LastGitStatus, nil
|
|
}
|
|
|
|
func toGitStatus(s *git.Status) *csapi.GitStatus {
|
|
limit := func(entries []string) []string {
|
|
if len(entries) > maxPendingChanges {
|
|
return append(entries[0:maxPendingChanges], fmt.Sprintf("... and %d more", len(entries)-maxPendingChanges))
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
return &csapi.GitStatus{
|
|
Branch: s.BranchHead,
|
|
LatestCommit: s.LatestCommit,
|
|
UncommitedFiles: limit(s.UncommitedFiles),
|
|
TotalUncommitedFiles: int64(len(s.UncommitedFiles)),
|
|
UntrackedFiles: limit(s.UntrackedFiles),
|
|
TotalUntrackedFiles: int64(len(s.UntrackedFiles)),
|
|
UnpushedCommits: limit(s.UnpushedCommits),
|
|
TotalUnpushedCommits: int64(len(s.UnpushedCommits)),
|
|
}
|
|
}
|
|
|
|
type persistentWorkspace struct {
|
|
*Workspace
|
|
State WorkspaceState `json:"state"`
|
|
}
|
|
|
|
func (s *Workspace) persistentStateLocation() string {
|
|
return filepath.Join(s.store.Location, fmt.Sprintf("%s.workspace.json", s.InstanceID))
|
|
}
|
|
|
|
func (s *Workspace) Persist() error {
|
|
s.stateLock.RLock()
|
|
fc, err := json.Marshal(persistentWorkspace{s, s.state})
|
|
s.stateLock.RUnlock()
|
|
if err != nil {
|
|
return xerrors.Errorf("cannot persist workspace: %w", err)
|
|
}
|
|
|
|
err = os.WriteFile(s.persistentStateLocation(), fc, 0644)
|
|
if err != nil {
|
|
return xerrors.Errorf("cannot persist workspace: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func loadWorkspace(ctx context.Context, path string) (sess *Workspace, err error) {
|
|
//nolint:ineffassign
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "loadWorkspace")
|
|
defer tracing.FinishSpan(span, &err)
|
|
|
|
fc, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot load session file: %w", err)
|
|
}
|
|
|
|
var p persistentWorkspace
|
|
err = json.Unmarshal(fc, &p)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot load session file: %w", err)
|
|
}
|
|
|
|
res := p.Workspace
|
|
res.NonPersistentAttrs = make(map[string]interface{})
|
|
res.state = p.State
|
|
res.operatingCondition = sync.NewCond(&sync.Mutex{})
|
|
|
|
return res, nil
|
|
}
|