mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
276 lines
8.4 KiB
Go
276 lines
8.4 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"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gitpod-io/gitpod/common-go/log"
|
|
"github.com/gitpod-io/gitpod/common-go/tracing"
|
|
|
|
"github.com/opentracing/opentracing-go"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
var (
|
|
// ErrAlreadyExists is returned when one tries to create a session which exists already
|
|
ErrAlreadyExists = fmt.Errorf("session already exists")
|
|
)
|
|
|
|
const (
|
|
workspaceFilePattern = "*.workspace.json"
|
|
)
|
|
|
|
// WorkspaceLivecycleHook can modify a workspace's non-persistent state.
|
|
// They're intended to start regular operations or initialize non-persistent objects.
|
|
type WorkspaceLivecycleHook func(ctx context.Context, ws *Workspace) error
|
|
|
|
// Store maintains multiple workspaces, can add, remove, update them and keep it's state persistent on disk
|
|
type Store struct {
|
|
Location string
|
|
|
|
hooks map[WorkspaceState][]WorkspaceLivecycleHook
|
|
workspaces map[string]*Workspace
|
|
workspacesLock sync.Mutex
|
|
}
|
|
|
|
// NewStore creates a new session store.
|
|
// If a store previously existed at that location, all previous workspaces are restored.
|
|
func NewStore(ctx context.Context, location string, hooks map[WorkspaceState][]WorkspaceLivecycleHook) (res *Store, err error) {
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "NewStore")
|
|
defer tracing.FinishSpan(span, &err)
|
|
|
|
res = &Store{
|
|
Location: location,
|
|
hooks: hooks,
|
|
workspaces: make(map[string]*Workspace),
|
|
}
|
|
|
|
existingWorkspaces, err := filepath.Glob(filepath.Join(location, workspaceFilePattern))
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot list existing workspaces: %w", err)
|
|
}
|
|
for _, sf := range existingWorkspaces {
|
|
session, err := loadWorkspace(ctx, sf)
|
|
if err != nil {
|
|
log.WithError(err).WithField("name", sf).Warn("cannot load session - this workspace is lost")
|
|
continue
|
|
}
|
|
session.store = res
|
|
|
|
err = res.runLifecycleHooks(ctx, session)
|
|
if err != nil {
|
|
log.WithError(err).WithField("name", sf).Warn("cannot load session - this workspace is lost")
|
|
continue
|
|
}
|
|
|
|
res.workspaces[session.InstanceID] = session
|
|
}
|
|
log.WithField("location", location).WithField("workspacesLoaded", len(res.workspaces)).WithField("workspacesOnDisk", len(existingWorkspaces)).Info("restored workspaces from disk")
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (s *Store) runLifecycleHooks(ctx context.Context, ws *Workspace) error {
|
|
hooks := s.hooks[ws.state]
|
|
log.WithFields(ws.OWI()).WithField("state", ws.state).WithField("hooks", len(hooks)).Debug("running lifecycle hooks")
|
|
|
|
for _, h := range hooks {
|
|
err := h(ctx, ws)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WorkspaceFactory creates a new fully configured workspace
|
|
type WorkspaceFactory func(ctx context.Context, location string) (*Workspace, error)
|
|
|
|
// NewWorkspace creates a new workspace in this store.
|
|
// CheckoutLocation is relative to location.
|
|
func (s *Store) NewWorkspace(ctx context.Context, instanceID, location string, create WorkspaceFactory) (res *Workspace, err error) {
|
|
s.workspacesLock.Lock()
|
|
defer s.workspacesLock.Unlock()
|
|
|
|
res, exists := s.workspaces[instanceID]
|
|
if exists {
|
|
return res, ErrAlreadyExists
|
|
}
|
|
|
|
res, err = create(ctx, location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.state = WorkspaceInitializing
|
|
if res.NonPersistentAttrs == nil {
|
|
res.NonPersistentAttrs = make(map[string]interface{})
|
|
}
|
|
res.operatingCondition = sync.NewCond(&sync.Mutex{})
|
|
res.store = s
|
|
err = s.runLifecycleHooks(ctx, res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.workspaces[instanceID] = res
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// Delete removes a session from this session store
|
|
func (s *Store) Delete(ctx context.Context, name string) (err error) {
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "Store.Delete")
|
|
defer tracing.FinishSpan(span, &err)
|
|
|
|
s.workspacesLock.Lock()
|
|
defer s.workspacesLock.Unlock()
|
|
|
|
session, exists := s.workspaces[name]
|
|
if !exists {
|
|
return nil
|
|
}
|
|
|
|
err = session.Dispose(ctx)
|
|
if err != nil {
|
|
return xerrors.Errorf("cannot delete session: %w", err)
|
|
}
|
|
|
|
delete(s.workspaces, name)
|
|
return nil
|
|
}
|
|
|
|
// Get retrieves a workspace
|
|
func (s *Store) Get(instanceID string) *Workspace {
|
|
s.workspacesLock.Lock()
|
|
defer s.workspacesLock.Unlock()
|
|
|
|
return s.workspaces[instanceID]
|
|
}
|
|
|
|
// StartHousekeeping starts garbage collection and regular cleanup.
|
|
// This function returns when the context is canceled.
|
|
func (s *Store) StartHousekeeping(ctx context.Context, interval time.Duration) {
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "Store.StartHousekeeping")
|
|
defer tracing.FinishSpan(span, nil)
|
|
log.WithField("interval", interval.String()).Debug("started workspace housekeeping")
|
|
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
run := true
|
|
for run {
|
|
var errs []error
|
|
select {
|
|
case <-ticker.C:
|
|
errs = s.doHousekeeping(ctx)
|
|
case <-ctx.Done():
|
|
run = false
|
|
break
|
|
}
|
|
|
|
for _, err := range errs {
|
|
log.WithError(err).Error("error during housekeeping")
|
|
}
|
|
}
|
|
|
|
span.Finish()
|
|
log.Debug("stopping workspace housekeeping")
|
|
}
|
|
|
|
func (s *Store) doHousekeeping(ctx context.Context) (errs []error) {
|
|
//nolint:ineffassign,staticcheck
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "doHousekeeping")
|
|
defer func() {
|
|
msgs := make([]string, len(errs))
|
|
for i, err := range errs {
|
|
msgs[i] = err.Error()
|
|
}
|
|
|
|
var err error
|
|
if len(msgs) > 0 {
|
|
err = xerrors.Errorf(strings.Join(msgs, ". "))
|
|
}
|
|
tracing.FinishSpan(span, &err)
|
|
}()
|
|
|
|
errs = make([]error, 0)
|
|
|
|
// find session files which are no longer needed
|
|
// TODO: This broke with FWB where the actual content can lay somewhere completely different
|
|
// Really we should not never remove workspaces from the ws-daemon side, but instead have
|
|
// ws-manager do that for us.
|
|
|
|
// existingWorkspaces, err := filepath.Glob(filepath.Join(s.Location, workspaceFilePattern))
|
|
// if err != nil {
|
|
// return []error{xerrors.Errorf("cannot list existing workspaces: %w", err)}
|
|
// }
|
|
// for _, ws := range existingWorkspaces {
|
|
// contentDirPath := strings.TrimSuffix(ws, ".workspace.json")
|
|
// if _, err := os.Stat(contentDirPath); err == nil || !os.IsNotExist(err) {
|
|
// // content directory still exists - we're good here
|
|
// continue
|
|
// }
|
|
|
|
// name := strings.TrimSuffix(filepath.Base(ws), ".json")
|
|
// err = s.Delete(ctx, name)
|
|
// if err != nil {
|
|
// log.WithError(err).Warn("Found workspace without workspace content, but cannot delete from store")
|
|
// errs = append(errs, err)
|
|
// continue
|
|
// }
|
|
|
|
// // if we didn't get the name right, or didn't have the session loaded (e.g. because it's broken),
|
|
// // we might still have to delete the JSON file.
|
|
// if _, err := os.Stat(contentDirPath); err == nil || !os.IsNotExist(err) {
|
|
// err := os.Remove(ws)
|
|
// if err != nil {
|
|
// log.WithError(err).Warn("Found workspace without workspace content, delete from store, but cannot delete from filesystem. We'll inadvertantly load this workspace again upon restart.")
|
|
// errs = append(errs, err)
|
|
// continue
|
|
// }
|
|
// }
|
|
|
|
// log.WithField("workspace", ws).Info("deleted workspaces without content directory")
|
|
// }
|
|
|
|
// find workspace directories which are left over.
|
|
// For the time being we won't do that because we're not sure we're the only ones working in this directory!
|
|
// Legacy ws-daemon will work here too.
|
|
//
|
|
// files, err := ioutil.ReadDir(s.Location)
|
|
// if err != nil {
|
|
// return []error{xerrors.Errorf("cannot list existing workspaces content directory: %w", err)}
|
|
// }
|
|
// for _, f := range files {
|
|
// if !f.IsDir() {
|
|
// continue
|
|
// }
|
|
|
|
// loc := filepath.Join(s.Location, f.Name())
|
|
// if _, err := os.Stat(fmt.Sprintf("%s.workspace.json", loc)); !os.IsNotExist(err) {
|
|
// continue
|
|
// }
|
|
|
|
// // We have found a workspace content directory without a workspace state file, which means we don't manage this folder.
|
|
// // Within the working area/location of a session store we must be the only one who creates directories, because we want to
|
|
// // make sure we don't leak files over time.
|
|
// err := os.RemoveAll(loc)
|
|
// if err != nil {
|
|
// log.WithError(err).Warn("Found workspace content directory without a corresponding state file, but could not delete the content directory")
|
|
// errs = append(errs, err)
|
|
// continue
|
|
// }
|
|
|
|
// log.WithField("directory", f.Name()).Info("deleted workspace content directory without corresponding state file")
|
|
// }
|
|
|
|
return errs
|
|
}
|