mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
127 lines
3.3 KiB
Go
127 lines
3.3 KiB
Go
// Copyright (c) 2023 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 controller
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gitpod-io/gitpod/common-go/log"
|
|
"github.com/gitpod-io/gitpod/common-go/tracing"
|
|
"github.com/opentracing/opentracing-go"
|
|
)
|
|
|
|
const minContentGCAge = 1 * time.Hour
|
|
|
|
type Housekeeping struct {
|
|
Location string
|
|
Interval time.Duration
|
|
}
|
|
|
|
func NewHousekeeping(location string, interval time.Duration) *Housekeeping {
|
|
return &Housekeeping{
|
|
Location: location,
|
|
Interval: interval,
|
|
}
|
|
}
|
|
|
|
func (h *Housekeeping) Start(ctx context.Context) {
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "Housekeeping.Start")
|
|
defer tracing.FinishSpan(span, nil)
|
|
log.WithField("interval", h.Interval.String()).Debug("started workspace housekeeping")
|
|
|
|
ticker := time.NewTicker(h.Interval)
|
|
defer ticker.Stop()
|
|
|
|
run := true
|
|
for run {
|
|
var errs []error
|
|
select {
|
|
case <-ticker.C:
|
|
errs = h.doHousekeeping(ctx)
|
|
case <-ctx.Done():
|
|
run = false
|
|
}
|
|
|
|
for _, err := range errs {
|
|
log.WithError(err).Error("error during housekeeping")
|
|
}
|
|
}
|
|
|
|
span.Finish()
|
|
log.Debug("stopping workspace housekeeping")
|
|
}
|
|
|
|
func (h *Housekeeping) doHousekeeping(ctx context.Context) (errs []error) {
|
|
span, _ := 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 = fmt.Errorf(strings.Join(msgs, ". "))
|
|
}
|
|
tracing.FinishSpan(span, &err)
|
|
}()
|
|
|
|
errs = make([]error, 0)
|
|
|
|
// Find workspace directories which are left over.
|
|
files, err := os.ReadDir(h.Location)
|
|
if err != nil {
|
|
return []error{fmt.Errorf("cannot list existing workspaces content directory: %w", err)}
|
|
}
|
|
|
|
for _, f := range files {
|
|
if !f.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// If this is the -daemon directory, make sure we assume the correct state file name
|
|
name := f.Name()
|
|
name = strings.TrimSuffix(name, string(filepath.Separator))
|
|
name = strings.TrimSuffix(name, "-daemon")
|
|
|
|
if _, err := os.Stat(filepath.Join(h.Location, fmt.Sprintf("%s.workspace.json", name))); !errors.Is(err, fs.ErrNotExist) {
|
|
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.
|
|
|
|
// For good measure we wait a while before deleting that directory.
|
|
nfo, err := f.Info()
|
|
if err != nil {
|
|
log.WithError(err).Warn("Found workspace content directory without a corresponding state file, but could not retrieve its info")
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
if time.Since(nfo.ModTime()) < minContentGCAge {
|
|
continue
|
|
}
|
|
|
|
err = os.RemoveAll(filepath.Join(h.Location, f.Name()))
|
|
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
|
|
}
|