85c93d3fde
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
1552 lines
57 KiB
Go
1552 lines
57 KiB
Go
package backendapp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
agentdocker "github.com/kandev/kandev/internal/agent/docker"
|
|
"github.com/kandev/kandev/internal/agent/runtime/activity"
|
|
"github.com/kandev/kandev/internal/db"
|
|
systemsettings "github.com/kandev/kandev/internal/system/settings"
|
|
storagepkg "github.com/kandev/kandev/internal/system/storage"
|
|
"github.com/kandev/kandev/internal/system/storage/databasestore"
|
|
"github.com/kandev/kandev/internal/system/storage/dockerstore"
|
|
"github.com/kandev/kandev/internal/system/storage/filescan"
|
|
"github.com/kandev/kandev/internal/system/storage/gocache"
|
|
"github.com/kandev/kandev/internal/system/storage/tempstore"
|
|
"github.com/kandev/kandev/internal/system/storage/workspaces"
|
|
"github.com/kandev/kandev/internal/worktree"
|
|
)
|
|
|
|
func TestStorageOverviewIncludesQuarantineAndManagedContainers(t *testing.T) {
|
|
home := t.TempDir()
|
|
for _, dir := range []string{filepath.Join(home, "tasks"), filepath.Join(home, "trash")} {
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := storagepkg.QuarantineEntry{
|
|
ID: "overview-entry", ResourceType: storagepkg.ResourceTypeGoCache,
|
|
OriginalPath: filepath.Join(home, "cache", "go-build"),
|
|
QuarantinePath: filepath.Join(home, "trash", "go-cache", "overview-entry"),
|
|
SizeBytes: 42, State: storagepkg.QuarantineStateQuarantined,
|
|
QuarantinedAt: time.Now().UTC(), DeleteAfter: time.Now().UTC().Add(time.Hour),
|
|
}
|
|
if err := store.CreateQuarantineEntry(context.Background(), &entry); err != nil {
|
|
t.Fatalf("CreateQuarantineEntry: %v", err)
|
|
}
|
|
docker := dockerstore.NewProvider(
|
|
&overviewDockerClient{usage: agentdocker.DiskUsage{ImageLayerBytes: 128, Containers: []agentdocker.ContainerUsage{
|
|
{ID: "managed", WritableBytes: 64, Labels: map[string]string{"kandev.managed": "true"}},
|
|
}}},
|
|
overviewContainerInventory{}, settings,
|
|
)
|
|
workspaceFactory := func(current storagepkg.StorageMaintenanceSettings) *workspaces.Provider {
|
|
return workspaces.New(workspaces.Config{
|
|
TasksRoot: filepath.Join(home, "tasks"), TrashRoot: filepath.Join(home, "trash"),
|
|
Inventory: overviewWorkspaceInventory{}, Store: store,
|
|
GracePeriod: time.Duration(current.OrphanGraceHours) * time.Hour,
|
|
Retention: time.Duration(current.QuarantineRetentionHours) * time.Hour,
|
|
})
|
|
}
|
|
overview := &storageOverview{
|
|
settings: settings, quarantine: store, workspaceFactory: workspaceFactory,
|
|
goCache: gocache.New(gocache.Config{
|
|
HomeDir: home, TrashDir: filepath.Join(home, "trash"), Settings: settings, Store: store,
|
|
}),
|
|
docker: docker, homeDir: home,
|
|
}
|
|
|
|
summary, err := overview.Summary(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Summary: %v", err)
|
|
}
|
|
quarantine, ok := summary.Quarantine.(storagepkg.QuarantineSummary)
|
|
if !ok || quarantine.Count != 1 || quarantine.SizeBytes != 42 {
|
|
t.Fatalf("quarantine summary = %#v", summary.Quarantine)
|
|
}
|
|
dockerSummary, ok := summary.Docker.(map[string]any)
|
|
if !ok || dockerSummary["managed_container_count"] != 1 ||
|
|
dockerSummary["managed_container_bytes"] != int64(64) ||
|
|
dockerSummary["image_layer_bytes"] != int64(128) {
|
|
t.Fatalf("docker summary = %#v", summary.Docker)
|
|
}
|
|
systemTemporary, ok := summary.SystemTemporary.(tempstore.Analysis)
|
|
if !ok || systemTemporary.Status != tempstore.StatusNotApplicable || systemTemporary.Roots == nil {
|
|
t.Fatalf("unavailable system temporary summary = %#v, want an empty roots array", summary.SystemTemporary)
|
|
}
|
|
|
|
overview.quarantine = failingQuarantineSummarizer{err: errors.New("quarantine unavailable")}
|
|
degraded, err := overview.Summary(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("degraded Summary: %v", err)
|
|
}
|
|
quarantineWarning, ok := degraded.Quarantine.(map[string]any)
|
|
if !ok || quarantineWarning["available"] != false || quarantineWarning["warning"] != "quarantine unavailable" {
|
|
t.Fatalf("degraded quarantine = %#v", degraded.Quarantine)
|
|
}
|
|
if _, ok := degraded.Workspaces.(workspaces.Analysis); !ok {
|
|
t.Fatalf("workspace summary should remain available, got %#v", degraded.Workspaces)
|
|
}
|
|
}
|
|
|
|
func TestSystemTemporaryConfigUsesDisposableE2ERoot(t *testing.T) {
|
|
root := t.TempDir()
|
|
t.Setenv("KANDEV_E2E_SYSTEM_TEMP_ROOT", root)
|
|
|
|
configured := systemTemporaryConfig(filescan.NewLimiter(1))
|
|
if configured.EffectiveRoot != root || configured.UnixRoot != root {
|
|
t.Fatalf("system temporary config = %#v, want disposable root %q", configured, root)
|
|
}
|
|
}
|
|
|
|
func TestStorageOverviewIncludesInformationalSystemTemporaryFootprint(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeStorageTestFile(t, filepath.Join(root, "temporary-file"), 23)
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
overview := &storageOverview{
|
|
settings: settings, quarantine: store,
|
|
workspaceFactory: func(current storagepkg.StorageMaintenanceSettings) *workspaces.Provider {
|
|
return workspaces.New(workspaces.Config{
|
|
TasksRoot: filepath.Join(root, "tasks"), TrashRoot: filepath.Join(root, "trash"),
|
|
Inventory: overviewWorkspaceInventory{}, Store: store,
|
|
GracePeriod: time.Duration(current.OrphanGraceHours) * time.Hour,
|
|
Retention: time.Duration(current.QuarantineRetentionHours) * time.Hour,
|
|
})
|
|
},
|
|
goCache: gocache.New(gocache.Config{HomeDir: root, TrashDir: filepath.Join(root, "trash"), Settings: settings, Store: store}),
|
|
docker: dockerstore.NewProvider(&overviewDockerClient{}, overviewContainerInventory{}, settings),
|
|
systemTemporary: tempstore.New(tempstore.Config{
|
|
GOOS: "windows", EffectiveRoot: root,
|
|
RootResolver: func(context.Context) ([]tempstore.RootCandidate, error) {
|
|
return []tempstore.RootCandidate{{RequestedPath: root}}, nil
|
|
},
|
|
}),
|
|
homeDir: root,
|
|
}
|
|
|
|
summary, err := overview.Summary(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Summary: %v", err)
|
|
}
|
|
temporary, ok := summary.SystemTemporary.(tempstore.Analysis)
|
|
if !ok || temporary.SizeBytes == nil || *temporary.SizeBytes != 23 || temporary.IncludedInTotal {
|
|
t.Fatalf("system temporary summary = %#v, want informational 23-byte measurement", summary.SystemTemporary)
|
|
}
|
|
}
|
|
|
|
func TestStorageOverviewIncludesDatabaseAndBackupMeasurements(t *testing.T) {
|
|
root := t.TempDir()
|
|
databasePath := filepath.Join(root, "data", "kandev.db")
|
|
writeStorageTestFile(t, databasePath, 11)
|
|
writeStorageTestFile(t, databasePath+"-wal", 7)
|
|
writeStorageTestFile(t, filepath.Join(root, "data", "backups", "manual.db"), 13)
|
|
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
overview := &storageOverview{
|
|
settings: settings,
|
|
quarantine: store,
|
|
database: databasestore.New(databasestore.Config{
|
|
Driver: "sqlite", DatabasePath: databasePath,
|
|
}),
|
|
workspaceFactory: func(current storagepkg.StorageMaintenanceSettings) *workspaces.Provider {
|
|
return workspaces.New(workspaces.Config{
|
|
TasksRoot: filepath.Join(root, "tasks"), TrashRoot: filepath.Join(root, "trash"),
|
|
Inventory: overviewWorkspaceInventory{}, Store: store,
|
|
GracePeriod: time.Duration(current.OrphanGraceHours) * time.Hour,
|
|
Retention: time.Duration(current.QuarantineRetentionHours) * time.Hour,
|
|
})
|
|
},
|
|
goCache: gocache.New(gocache.Config{
|
|
HomeDir: root, TrashDir: filepath.Join(root, "trash"), Settings: settings, Store: store,
|
|
}),
|
|
docker: dockerstore.NewProvider(&overviewDockerClient{}, overviewContainerInventory{}, settings),
|
|
homeDir: root,
|
|
}
|
|
|
|
summary, err := overview.Summary(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Summary: %v", err)
|
|
}
|
|
database, ok := summary.Database.(databasestore.Measurement)
|
|
if !ok {
|
|
t.Fatalf("database summary = %#v, want databasestore.Measurement", summary.Database)
|
|
}
|
|
if database.SizeBytes == nil || *database.SizeBytes != 18 {
|
|
t.Fatalf("database summary = %#v, want 18 bytes", database)
|
|
}
|
|
backups, ok := summary.DatabaseBackups.(databasestore.Measurement)
|
|
if !ok {
|
|
t.Fatalf("backup summary = %#v, want databasestore.Measurement", summary.DatabaseBackups)
|
|
}
|
|
if backups.SizeBytes == nil || *backups.SizeBytes != 13 {
|
|
t.Fatalf("backup summary = %#v, want 13 bytes", backups)
|
|
}
|
|
}
|
|
|
|
func TestStorageOverviewDoesNotSuppressDatabaseUnderFormerDefaultGoCache(t *testing.T) {
|
|
root := t.TempDir()
|
|
formerDefaultCache := filepath.Join(root, "cache", "go-build")
|
|
databasePath := filepath.Join(formerDefaultCache, "kandev.db")
|
|
writeStorageTestFile(t, databasePath, 11)
|
|
|
|
adoptedCache := filepath.Join(t.TempDir(), "adopted-go-build")
|
|
userCache := filepath.Join(t.TempDir(), "user-go-build")
|
|
t.Setenv("GOCACHE", userCache)
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
if _, err := settings.AdoptGoCachePath(context.Background(), adoptedCache); err != nil {
|
|
t.Fatalf("AdoptGoCachePath: %v", err)
|
|
}
|
|
|
|
overview := &storageOverview{
|
|
settings: settings,
|
|
quarantine: store,
|
|
database: databasestore.New(databasestore.Config{
|
|
Driver: "sqlite", DatabasePath: databasePath, ExistingRoots: storageExistingMeasurementRoots(root),
|
|
}),
|
|
workspaceFactory: func(current storagepkg.StorageMaintenanceSettings) *workspaces.Provider {
|
|
return workspaces.New(workspaces.Config{
|
|
TasksRoot: filepath.Join(root, "tasks"), TrashRoot: filepath.Join(root, "trash"),
|
|
Inventory: overviewWorkspaceInventory{}, Store: store,
|
|
GracePeriod: time.Duration(current.OrphanGraceHours) * time.Hour,
|
|
Retention: time.Duration(current.QuarantineRetentionHours) * time.Hour,
|
|
})
|
|
},
|
|
goCache: gocache.New(gocache.Config{
|
|
HomeDir: root, TrashDir: filepath.Join(root, "trash"), Settings: settings, Store: store,
|
|
}),
|
|
docker: dockerstore.NewProvider(&overviewDockerClient{}, overviewContainerInventory{}, settings),
|
|
homeDir: root,
|
|
}
|
|
|
|
summary, err := overview.Summary(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Summary: %v", err)
|
|
}
|
|
database, ok := summary.Database.(databasestore.Measurement)
|
|
if !ok {
|
|
t.Fatalf("database summary = %#v, want databasestore.Measurement", summary.Database)
|
|
}
|
|
if !database.IncludedInTotal {
|
|
t.Fatalf("database attribution = %#v, want included because the effective cache roots are elsewhere", database)
|
|
}
|
|
}
|
|
|
|
func TestStorageOverviewReportsProgressForEachSource(t *testing.T) {
|
|
settings, _ := newStorageMaintenanceStores(t)
|
|
docker := dockerstore.NewProvider(
|
|
&overviewDockerClient{}, overviewContainerInventory{}, settings,
|
|
)
|
|
events := make(chan storagepkg.OverviewProgress, 16)
|
|
overview := &storageOverview{
|
|
settings: settings,
|
|
quarantine: failingQuarantineSummarizer{err: errors.New("quarantine unavailable")},
|
|
workspaceAnalyze: func(context.Context, storagepkg.StorageMaintenanceSettings) (workspaces.Analysis, error) {
|
|
return workspaces.Analysis{TotalBytes: 10}, nil
|
|
},
|
|
goCacheAnalyze: func(context.Context) (gocache.Analysis, error) {
|
|
return gocache.Analysis{SizeBytes: 20}, nil
|
|
},
|
|
docker: docker,
|
|
}
|
|
|
|
if _, err := overview.SummaryWithProgress(context.Background(), func(progress storagepkg.OverviewProgress) {
|
|
events <- progress
|
|
}); err != nil {
|
|
t.Fatalf("SummaryWithProgress: %v", err)
|
|
}
|
|
seen := make(map[string]map[storagepkg.SourceStateName]bool)
|
|
for len(events) > 0 {
|
|
progress := <-events
|
|
if seen[progress.Source] == nil {
|
|
seen[progress.Source] = make(map[storagepkg.SourceStateName]bool)
|
|
}
|
|
seen[progress.Source][progress.State] = true
|
|
}
|
|
for _, source := range []string{
|
|
storagepkg.StorageSourceWorkspaces,
|
|
storagepkg.StorageSourceGoCache,
|
|
storagepkg.StorageSourceQuarantine,
|
|
storagepkg.StorageSourceTemporaryArtifacts,
|
|
storagepkg.StorageSourceSystemTemporary,
|
|
storagepkg.StorageSourceDocker,
|
|
storagepkg.StorageSourceDatabase,
|
|
storagepkg.StorageSourceDatabaseBackups,
|
|
} {
|
|
if !seen[source][storagepkg.SourceStateScanning] {
|
|
t.Fatalf("source %q did not report scanning progress: %#v", source, seen[source])
|
|
}
|
|
}
|
|
if !seen[storagepkg.StorageSourceQuarantine][storagepkg.SourceStateFailed] {
|
|
t.Fatalf("quarantine source did not report failure: %#v", seen[storagepkg.StorageSourceQuarantine])
|
|
}
|
|
}
|
|
|
|
func writeStorageTestFile(t *testing.T, path string, size int64) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := file.Truncate(size); err != nil {
|
|
_ = file.Close()
|
|
t.Fatal(err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestStorageProgressReporterCompletionPreservesFilesystemCounters(t *testing.T) {
|
|
var events []storagepkg.OverviewProgress
|
|
reporter := newStorageProgressReporter(func(progress storagepkg.OverviewProgress) {
|
|
events = append(events, progress)
|
|
})
|
|
reporter.filesystem(storagepkg.StorageSourceWorkspaces)(filescan.Progress{
|
|
Phase: filescan.PartitionCompleted, CompletedPartitions: 2, TotalPartitions: 3,
|
|
BytesScanned: 42,
|
|
})
|
|
reporter.complete(storagepkg.StorageSourceWorkspaces, map[string]any{"total_bytes": int64(42)}, nil)
|
|
|
|
if len(events) != 2 {
|
|
t.Fatalf("progress event count = %d, want 2", len(events))
|
|
}
|
|
completion := events[1]
|
|
if completion.CompletedItems != 2 || completion.BytesScanned != 42 {
|
|
t.Fatalf("completion progress = %#v, want filesystem counters", completion)
|
|
}
|
|
if completion.TotalItems == nil || *completion.TotalItems != 3 {
|
|
t.Fatalf("completion total_items = %v, want 3", completion.TotalItems)
|
|
}
|
|
}
|
|
|
|
func TestStorageCleanupProvidersIncludeWorkspaceDependencyCleanup(t *testing.T) {
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
home := t.TempDir()
|
|
workspaceFactory := func(storagepkg.StorageMaintenanceSettings) *workspaces.Provider {
|
|
return workspaces.New(workspaces.Config{TasksRoot: filepath.Join(home, "tasks"), Store: store})
|
|
}
|
|
providers := storageCleanupProviders(settings, workspaceFactory, nil, nil, nil, nil)
|
|
for _, provider := range providers {
|
|
if provider.Name() == workspaceDependenciesProviderName {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("storage cleanup providers did not include %s", workspaceDependenciesProviderName)
|
|
}
|
|
|
|
func TestStorageCleanupProvidersIncludeArchivedManagedBranches(t *testing.T) {
|
|
const providerName = "archived_managed_branches"
|
|
providers := storageCleanupProviders(nil, nil, nil, nil, nil, nil)
|
|
for _, provider := range providers {
|
|
if provider.Name() == providerName {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("storage cleanup providers did not include %s", providerName)
|
|
}
|
|
|
|
type recordingArchivedBranchMaintainer struct {
|
|
limit int
|
|
receipt worktree.BranchCleanupReceipt
|
|
}
|
|
|
|
func (m *recordingArchivedBranchMaintainer) MaintainArchivedBranches(
|
|
_ context.Context, limit int,
|
|
) (worktree.BranchCleanupReceipt, error) {
|
|
m.limit = limit
|
|
return m.receipt, nil
|
|
}
|
|
|
|
func TestArchivedManagedBranchesProviderUsesBoundedManagerReceipt(t *testing.T) {
|
|
maintainer := &recordingArchivedBranchMaintainer{receipt: worktree.BranchCleanupReceipt{
|
|
Attempted: 2, Deleted: 1, Retained: 1,
|
|
RetainedReasons: map[worktree.BranchRetentionReason]int{worktree.RetainedNotIntegrated: 1},
|
|
}}
|
|
provider := archivedManagedBranchesCleanupProvider{maintainer: maintainer}
|
|
result, err := provider.Cleanup(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("archived branch cleanup: %v", err)
|
|
}
|
|
if maintainer.limit != worktree.ArchivedBranchMaintenanceBatchLimit {
|
|
t.Fatalf("maintenance limit = %d, want %d", maintainer.limit, worktree.ArchivedBranchMaintenanceBatchLimit)
|
|
}
|
|
if result["attempted"] != float64(2) || result["deleted"] != float64(1) || result["retained"] != float64(1) {
|
|
t.Fatalf("maintenance result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestWorkspaceDependencyCleanupProviderIsDefaultOff(t *testing.T) {
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
home := t.TempDir()
|
|
factoryCalls := 0
|
|
workspaceFactory := func(storagepkg.StorageMaintenanceSettings) *workspaces.Provider {
|
|
factoryCalls++
|
|
return workspaces.New(workspaces.Config{TasksRoot: filepath.Join(home, "tasks"), Store: store})
|
|
}
|
|
var dependencyProvider storagepkg.CleanupProvider
|
|
for _, provider := range storageCleanupProviders(settings, workspaceFactory, nil, nil, nil, nil) {
|
|
if provider.Name() == workspaceDependenciesProviderName {
|
|
dependencyProvider = provider
|
|
break
|
|
}
|
|
}
|
|
if dependencyProvider == nil {
|
|
t.Fatal("workspace dependency provider is missing")
|
|
}
|
|
if result, err := dependencyProvider.Cleanup(context.Background()); err != nil || result != nil {
|
|
t.Fatalf("default-off cleanup = (%#v, %v), want no-op", result, err)
|
|
}
|
|
if factoryCalls != 0 {
|
|
t.Fatalf("workspace factory calls = %d, want 0 while disabled", factoryCalls)
|
|
}
|
|
|
|
enabled := storagepkg.DefaultSettings()
|
|
enabled.Workspaces.DependencyCleanupEnabled = true
|
|
if _, err := settings.SaveSettings(context.Background(), enabled); err != nil {
|
|
t.Fatalf("enable dependency cleanup: %v", err)
|
|
}
|
|
if _, err := dependencyProvider.Cleanup(context.Background()); err == nil {
|
|
t.Fatal("enabled dependency cleanup unexpectedly succeeded without complete inventory")
|
|
}
|
|
if factoryCalls != 1 {
|
|
t.Fatalf("workspace factory calls = %d, want 1 after enabling", factoryCalls)
|
|
}
|
|
}
|
|
|
|
func TestStorageOverviewStartsIndependentMeasurementsTogether(t *testing.T) {
|
|
home := t.TempDir()
|
|
for _, dir := range []string{filepath.Join(home, "tasks"), filepath.Join(home, "trash")} {
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
t.Setenv("GOCACHE", filepath.Join(home, "go-cache"))
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
workspaceStarted, workspaceRelease := make(chan struct{}), make(chan struct{})
|
|
goCacheStarted, goCacheRelease := make(chan struct{}), make(chan struct{})
|
|
quarantine := &blockingOverviewQuarantine{
|
|
started: make(chan struct{}), release: make(chan struct{}),
|
|
}
|
|
dockerClient := &blockingOverviewDockerClient{
|
|
started: make(chan struct{}), release: make(chan struct{}),
|
|
}
|
|
var releaseOnce sync.Once
|
|
releaseMeasurements := func() {
|
|
releaseOnce.Do(func() {
|
|
close(workspaceRelease)
|
|
close(goCacheRelease)
|
|
close(quarantine.release)
|
|
close(dockerClient.release)
|
|
})
|
|
}
|
|
t.Cleanup(releaseMeasurements)
|
|
docker := dockerstore.NewProvider(dockerClient, overviewContainerInventory{}, settings)
|
|
overview := &storageOverview{
|
|
settings: settings, quarantine: quarantine,
|
|
workspaceAnalyze: func(context.Context, storagepkg.StorageMaintenanceSettings) (workspaces.Analysis, error) {
|
|
close(workspaceStarted)
|
|
<-workspaceRelease
|
|
return workspaces.Analysis{}, nil
|
|
},
|
|
goCacheAnalyze: func(context.Context) (gocache.Analysis, error) {
|
|
close(goCacheStarted)
|
|
<-goCacheRelease
|
|
return gocache.Analysis{}, nil
|
|
},
|
|
workspaceFactory: func(current storagepkg.StorageMaintenanceSettings) *workspaces.Provider {
|
|
return workspaces.New(workspaces.Config{
|
|
TasksRoot: filepath.Join(home, "tasks"), TrashRoot: filepath.Join(home, "trash"),
|
|
Inventory: overviewWorkspaceInventory{}, Store: store,
|
|
GracePeriod: time.Duration(current.OrphanGraceHours) * time.Hour,
|
|
Retention: time.Duration(current.QuarantineRetentionHours) * time.Hour,
|
|
})
|
|
},
|
|
goCache: gocache.New(gocache.Config{
|
|
HomeDir: home, TrashDir: filepath.Join(home, "trash"), Settings: settings, Store: store,
|
|
}),
|
|
docker: docker, homeDir: home,
|
|
}
|
|
|
|
resultCh := make(chan struct {
|
|
summary storagepkg.Summary
|
|
err error
|
|
}, 1)
|
|
go func() {
|
|
summary, err := overview.Summary(context.Background())
|
|
resultCh <- struct {
|
|
summary storagepkg.Summary
|
|
err error
|
|
}{summary: summary, err: err}
|
|
}()
|
|
|
|
for _, measurement := range []struct {
|
|
name string
|
|
started <-chan struct{}
|
|
}{
|
|
{name: "workspace", started: workspaceStarted},
|
|
{name: "Go cache", started: goCacheStarted},
|
|
{name: "quarantine", started: quarantine.started},
|
|
{name: "Docker", started: dockerClient.started},
|
|
} {
|
|
select {
|
|
case <-measurement.started:
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("%s measurement did not start before the barrier was released", measurement.name)
|
|
}
|
|
}
|
|
releaseMeasurements()
|
|
|
|
select {
|
|
case result := <-resultCh:
|
|
if result.err != nil {
|
|
t.Fatalf("Summary: %v", result.err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Summary did not finish")
|
|
}
|
|
}
|
|
|
|
type blockingOverviewQuarantine struct {
|
|
started chan struct{}
|
|
release chan struct{}
|
|
}
|
|
|
|
func (s *blockingOverviewQuarantine) SummarizeQuarantine(context.Context) (storagepkg.QuarantineSummary, error) {
|
|
close(s.started)
|
|
<-s.release
|
|
return storagepkg.QuarantineSummary{}, nil
|
|
}
|
|
|
|
type blockingOverviewDockerClient struct {
|
|
started chan struct{}
|
|
release chan struct{}
|
|
}
|
|
|
|
func (c *blockingOverviewDockerClient) Ping(context.Context) error { return nil }
|
|
func (c *blockingOverviewDockerClient) ListContainers(context.Context, map[string]string) ([]agentdocker.ContainerInfo, error) {
|
|
return nil, nil
|
|
}
|
|
func (c *blockingOverviewDockerClient) RemoveContainer(context.Context, string, bool) error {
|
|
return nil
|
|
}
|
|
func (c *blockingOverviewDockerClient) DiskUsage(context.Context) (agentdocker.DiskUsage, error) {
|
|
close(c.started)
|
|
<-c.release
|
|
return agentdocker.DiskUsage{}, nil
|
|
}
|
|
func (c *blockingOverviewDockerClient) PruneBuildCache(context.Context, agentdocker.BuildCachePruneOptions) (agentdocker.PruneResult, error) {
|
|
return agentdocker.PruneResult{}, nil
|
|
}
|
|
func (c *blockingOverviewDockerClient) PruneUnusedImages(context.Context, time.Time) (agentdocker.PruneResult, error) {
|
|
return agentdocker.PruneResult{}, nil
|
|
}
|
|
|
|
type failingQuarantineSummarizer struct{ err error }
|
|
|
|
func (s failingQuarantineSummarizer) SummarizeQuarantine(context.Context) (storagepkg.QuarantineSummary, error) {
|
|
return storagepkg.QuarantineSummary{}, s.err
|
|
}
|
|
|
|
type overviewWorkspaceInventory struct{}
|
|
|
|
func (overviewWorkspaceInventory) LoadWorkspaceInventory(context.Context) (workspaces.Inventory, error) {
|
|
return workspaces.Inventory{Complete: true}, nil
|
|
}
|
|
|
|
type overviewContainerInventory struct{}
|
|
|
|
func (overviewContainerInventory) ContainerTaskRemovable(context.Context, string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
type overviewDockerClient struct{ usage agentdocker.DiskUsage }
|
|
|
|
func (c *overviewDockerClient) Ping(context.Context) error { return nil }
|
|
func (c *overviewDockerClient) ListContainers(context.Context, map[string]string) ([]agentdocker.ContainerInfo, error) {
|
|
return nil, nil
|
|
}
|
|
func (c *overviewDockerClient) RemoveContainer(context.Context, string, bool) error { return nil }
|
|
func (c *overviewDockerClient) DiskUsage(context.Context) (agentdocker.DiskUsage, error) {
|
|
return c.usage, nil
|
|
}
|
|
func (c *overviewDockerClient) PruneBuildCache(context.Context, agentdocker.BuildCachePruneOptions) (agentdocker.PruneResult, error) {
|
|
return agentdocker.PruneResult{}, nil
|
|
}
|
|
func (c *overviewDockerClient) PruneUnusedImages(context.Context, time.Time) (agentdocker.PruneResult, error) {
|
|
return agentdocker.PruneResult{}, nil
|
|
}
|
|
|
|
func TestWorkspaceQuarantineControllerRestoresTaskUsingCurrentSettings(t *testing.T) {
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
want := storagepkg.DefaultSettings()
|
|
want.OrphanGraceHours = 48
|
|
if _, err := settings.SaveSettings(context.Background(), want); err != nil {
|
|
t.Fatalf("SaveSettings: %v", err)
|
|
}
|
|
var captured storagepkg.StorageMaintenanceSettings
|
|
controller := &workspaceQuarantineController{
|
|
settings: settings,
|
|
factory: func(current storagepkg.StorageMaintenanceSettings) *workspaces.Provider {
|
|
captured = current
|
|
return workspaces.New(workspaces.Config{Store: store})
|
|
},
|
|
}
|
|
|
|
recovery := controller.RestoreTask(context.Background(), "task-1")
|
|
if captured.OrphanGraceHours != want.OrphanGraceHours {
|
|
t.Fatalf("factory settings orphan_grace_hours = %d, want %d", captured.OrphanGraceHours, want.OrphanGraceHours)
|
|
}
|
|
if recovery.TaskID != "task-1" || recovery.Status != "not_found" {
|
|
t.Fatalf("recovery = %#v, want task-1 not_found", recovery)
|
|
}
|
|
}
|
|
|
|
func TestWorkspaceQuarantineControllerMapsRestoreConflict(t *testing.T) {
|
|
home := t.TempDir()
|
|
tasksRoot := filepath.Join(home, "tasks")
|
|
original := filepath.Join(tasksRoot, "task-1")
|
|
quarantined := filepath.Join(home, "trash", "tasks", "entry-1")
|
|
for _, path := range []string{original, quarantined} {
|
|
if err := os.MkdirAll(path, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := storagepkg.QuarantineEntry{
|
|
ID: "entry-1", ResourceType: storagepkg.ResourceTypeTaskWorkspace,
|
|
OriginalPath: original, QuarantinePath: quarantined,
|
|
State: storagepkg.QuarantineStateQuarantined,
|
|
QuarantinedAt: time.Now().UTC(), DeleteAfter: time.Now().UTC().Add(time.Hour),
|
|
}
|
|
if err := store.CreateQuarantineEntry(context.Background(), &entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
controller := &workspaceQuarantineController{
|
|
settings: settings, store: store,
|
|
factory: func(current storagepkg.StorageMaintenanceSettings) *workspaces.Provider {
|
|
return workspaces.New(workspaces.Config{
|
|
TasksRoot: tasksRoot, TrashRoot: filepath.Join(home, "trash"), Store: store,
|
|
GracePeriod: time.Duration(current.OrphanGraceHours) * time.Hour,
|
|
Retention: time.Duration(current.QuarantineRetentionHours) * time.Hour,
|
|
})
|
|
},
|
|
}
|
|
|
|
_, err := controller.Restore(context.Background(), entry.ID)
|
|
if !errors.Is(err, storagepkg.ErrConflict) {
|
|
t.Fatalf("Restore error = %v, want storage ErrConflict", err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerRestoresGoCache(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(quarantined, "artifact"), []byte("cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := createGoCacheQuarantineEntry(t, store, original, quarantined, time.Now().UTC().Add(time.Hour))
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
restored, err := controller.Restore(context.Background(), entry.ID)
|
|
if err != nil {
|
|
t.Fatalf("Restore: %v", err)
|
|
}
|
|
if restored.State != storagepkg.QuarantineStateRestored {
|
|
t.Fatalf("state = %q, want restored", restored.State)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(original, "artifact")); err != nil {
|
|
t.Fatalf("restored cache artifact: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerRestoresGoCacheOverEmptyReplacement(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(quarantined, "artifact"), []byte("cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
current, err := settings.GetSettings(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
current.GoCache.Enabled = true
|
|
if _, err := settings.SaveSettings(context.Background(), current); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
provider := gocache.New(gocache.Config{
|
|
HomeDir: home, TrashDir: filepath.Join(home, "trash"), Settings: settings,
|
|
})
|
|
if _, err := provider.ExecutionEnvironment(context.Background()); err != nil {
|
|
t.Fatalf("create managed replacement: %v", err)
|
|
}
|
|
entry := createGoCacheQuarantineEntry(t, store, original, quarantined, time.Now().UTC().Add(time.Hour))
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
restored, err := controller.Restore(context.Background(), entry.ID)
|
|
if err != nil {
|
|
t.Fatalf("Restore: %v", err)
|
|
}
|
|
if restored.State != storagepkg.QuarantineStateRestored {
|
|
t.Fatalf("state = %q, want restored", restored.State)
|
|
}
|
|
if data, err := os.ReadFile(filepath.Join(original, "artifact")); err != nil || string(data) != "cache" {
|
|
t.Fatalf("restored cache artifact: data=%q err=%v", data, err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerRetainsGoCacheWhileTaskActivityIsRunning(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
artifact := filepath.Join(quarantined, "artifact")
|
|
if err := os.WriteFile(artifact, []byte("cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
current, err := settings.GetSettings(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
current.GoCache.Enabled = true
|
|
if _, err := settings.SaveSettings(context.Background(), current); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
provider := gocache.New(gocache.Config{
|
|
HomeDir: home, TrashDir: filepath.Join(home, "trash"), Settings: settings,
|
|
})
|
|
if _, err := provider.ExecutionEnvironment(context.Background()); err != nil {
|
|
t.Fatalf("create managed replacement: %v", err)
|
|
}
|
|
entry := createGoCacheQuarantineEntry(t, store, original, quarantined, time.Now().UTC().Add(time.Hour))
|
|
coordinator := activity.NewCoordinator(activity.Options{})
|
|
taskLease, err := coordinator.AcquireTask(context.Background(), activity.KindExecutionRunning)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer taskLease.Release()
|
|
controller := &workspaceQuarantineController{
|
|
settings: settings, store: store, homeDir: home, activity: coordinator,
|
|
}
|
|
|
|
_, err = controller.Restore(context.Background(), entry.ID)
|
|
var busy *storagepkg.BusyError
|
|
if !errors.As(err, &busy) {
|
|
t.Fatalf("Restore error = %v, want BusyError", err)
|
|
}
|
|
if busy.ForceAvailable || len(busy.Resources) != 1 || busy.Resources[0].Label == "" {
|
|
t.Fatalf("busy response = %#v, want labeled non-overridable resource", busy)
|
|
}
|
|
if _, err := os.Stat(artifact); err != nil {
|
|
t.Fatalf("quarantined cache changed while task active: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerRestoresAdoptedExternalGoCache(t *testing.T) {
|
|
root := t.TempDir()
|
|
home := filepath.Join(root, "home")
|
|
original := filepath.Join(root, "external", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
for _, path := range []string{home, original, quarantined} {
|
|
if err := os.MkdirAll(path, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(quarantined, "artifact"), []byte("cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Remove(original); err != nil {
|
|
t.Fatalf("remove recreated cache destination: %v", err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
if _, err := settings.AdoptGoCachePath(context.Background(), original); err != nil {
|
|
t.Fatalf("AdoptGoCachePath: %v", err)
|
|
}
|
|
entry := createGoCacheQuarantineEntry(
|
|
t, store, original, quarantined, time.Now().UTC().Add(time.Hour),
|
|
)
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
restored, err := controller.Restore(context.Background(), entry.ID)
|
|
if err != nil {
|
|
t.Fatalf("Restore: %v", err)
|
|
}
|
|
if restored.State != storagepkg.QuarantineStateRestored {
|
|
t.Fatalf("state = %q, want restored", restored.State)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(original, "artifact")); err != nil {
|
|
t.Fatalf("restored external cache artifact: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerRetriesFailedGoCacheRestore(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(quarantined, "artifact"), []byte("cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := createGoCacheQuarantineEntry(t, store, original, quarantined, time.Now().UTC().Add(time.Hour))
|
|
markGoCacheQuarantineFailed(t, store, entry.ID)
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
restored, err := controller.Restore(context.Background(), entry.ID)
|
|
if err != nil {
|
|
t.Fatalf("Restore: %v", err)
|
|
}
|
|
if restored.State != storagepkg.QuarantineStateRestored {
|
|
t.Fatalf("state = %q, want restored", restored.State)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(original, "artifact")); err != nil {
|
|
t.Fatalf("restored cache artifact: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerRetriesFailedGoCacheDelete(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := createGoCacheQuarantineEntry(t, store, original, quarantined, time.Now().UTC().Add(-time.Hour))
|
|
markGoCacheQuarantineFailed(t, store, entry.ID)
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
deleted, err := controller.PermanentDelete(context.Background(), entry.ID, "DELETE")
|
|
if err != nil {
|
|
t.Fatalf("PermanentDelete: %v", err)
|
|
}
|
|
if deleted.State != storagepkg.QuarantineStateDeleted {
|
|
t.Fatalf("state = %q, want deleted", deleted.State)
|
|
}
|
|
if _, err := os.Stat(quarantined); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("quarantine path still exists: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerForceDeletesProtectedGoCache(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(quarantined, "artifact"), []byte("cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := createGoCacheQuarantineEntry(t, store, original, quarantined, time.Now().UTC().Add(time.Hour))
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
if _, err := controller.PermanentDeleteForce(context.Background(), entry.ID, "DELETE"); !errors.Is(err, storagepkg.ErrForceDeleteConfirmation) {
|
|
t.Fatalf("wrong force confirmation error = %v, want %v", err, storagepkg.ErrForceDeleteConfirmation)
|
|
}
|
|
deleted, err := controller.PermanentDeleteForce(context.Background(), entry.ID, "DELETE ALL NOW")
|
|
if err != nil {
|
|
t.Fatalf("PermanentDeleteForce: %v", err)
|
|
}
|
|
if deleted.State != storagepkg.QuarantineStateDeleted {
|
|
t.Fatalf("state = %q, want deleted", deleted.State)
|
|
}
|
|
if _, err := os.Stat(quarantined); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("quarantine path still exists: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerPurgeEligibleReportsProtectedAndDeleted(t *testing.T) {
|
|
home := t.TempDir()
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
protected := storagepkg.QuarantineEntry{
|
|
ID: "protected-workspace", ResourceType: storagepkg.ResourceTypeTaskWorkspace,
|
|
OriginalPath: filepath.Join(home, "tasks", "protected"),
|
|
QuarantinePath: filepath.Join(home, "trash", "tasks", "protected-workspace"),
|
|
SizeBytes: 17, State: storagepkg.QuarantineStateQuarantined,
|
|
QuarantinedAt: time.Now().UTC(), DeleteAfter: time.Now().UTC().Add(time.Hour),
|
|
}
|
|
if err := os.MkdirAll(protected.QuarantinePath, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.CreateQuarantineEntry(context.Background(), &protected); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
eligible := createGoCacheQuarantineEntryWithID(
|
|
t, store, home, "eligible-cache", time.Now().UTC().Add(-time.Hour),
|
|
)
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
result, err := controller.Purge(context.Background(), storagepkg.QuarantinePurgeScopeEligible, "DELETE ELIGIBLE")
|
|
if err != nil {
|
|
t.Fatalf("Purge: %v", err)
|
|
}
|
|
if result.Considered != 2 || result.Deleted != 1 || result.Protected != 1 || result.Failed != 0 {
|
|
t.Fatalf("purge result = %#v, want considered=2 deleted=1 protected=1 failed=0", result)
|
|
}
|
|
if result.DeletedBytes != eligible.SizeBytes || result.ProtectedBytes != protected.SizeBytes {
|
|
t.Fatalf("purge bytes = deleted:%d protected:%d, want deleted:%d protected:%d", result.DeletedBytes, result.ProtectedBytes, eligible.SizeBytes, protected.SizeBytes)
|
|
}
|
|
if _, err := os.Stat(eligible.QuarantinePath); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("eligible quarantine path still exists: %v", err)
|
|
}
|
|
if _, err := os.Stat(protected.QuarantinePath); err != nil {
|
|
t.Fatalf("protected quarantine path changed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineCleanupProviderPurgesEligibleEntries(t *testing.T) {
|
|
purger := &recordingQuarantinePurger{}
|
|
provider := quarantineCleanupProvider{purger: purger}
|
|
|
|
result, err := provider.Cleanup(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Cleanup: %v", err)
|
|
}
|
|
if purger.scope != storagepkg.QuarantinePurgeScopeEligible || purger.confirmation != "DELETE ELIGIBLE" {
|
|
t.Fatalf("purge request = scope:%q confirmation:%q", purger.scope, purger.confirmation)
|
|
}
|
|
if result["deleted"] != float64(2) || result["protected"] != float64(1) {
|
|
t.Fatalf("provider result = %#v, want deleted=2 protected=1", result)
|
|
}
|
|
}
|
|
|
|
func TestStorageCleanupProvidersIncludesQuarantineProvider(t *testing.T) {
|
|
providers := storageCleanupProviders(nil, nil, nil, nil, &recordingQuarantinePurger{}, nil)
|
|
if len(providers) != 8 {
|
|
t.Fatalf("provider count = %d, want 8", len(providers))
|
|
}
|
|
if providers[0].Name() != "quarantine" {
|
|
t.Fatalf("first provider = %q, want quarantine", providers[0].Name())
|
|
}
|
|
if providers[1].Name() != archivedManagedBranchesProviderName || providers[2].Name() != "workspaces" ||
|
|
providers[3].Name() != workspaceDependenciesProviderName || providers[4].Name() != "go_cache" {
|
|
t.Fatalf("provider order = %q, %q, %q, %q, want archived branches, workspaces, workspace_dependencies, go_cache",
|
|
providers[1].Name(), providers[2].Name(), providers[3].Name(), providers[4].Name())
|
|
}
|
|
}
|
|
|
|
type recordingQuarantinePurger struct {
|
|
scope storagepkg.QuarantinePurgeScope
|
|
confirmation string
|
|
}
|
|
|
|
func (p *recordingQuarantinePurger) Purge(
|
|
_ context.Context,
|
|
scope storagepkg.QuarantinePurgeScope,
|
|
confirmation string,
|
|
) (storagepkg.QuarantinePurgeResult, error) {
|
|
p.scope = scope
|
|
p.confirmation = confirmation
|
|
return storagepkg.QuarantinePurgeResult{Deleted: 2, Protected: 1}, nil
|
|
}
|
|
|
|
func TestQuarantineControllerDoesNotTreatPopulatedReplacementAsRestoredPayload(t *testing.T) {
|
|
states := []storagepkg.QuarantineState{
|
|
storagepkg.QuarantineStateQuarantined,
|
|
storagepkg.QuarantineStateFailed,
|
|
}
|
|
for _, state := range states {
|
|
t.Run(string(state), func(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(original, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
artifact := filepath.Join(original, "replacement-artifact")
|
|
if err := os.WriteFile(artifact, []byte("active cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := createGoCacheQuarantineEntry(
|
|
t, store, original, quarantined, time.Now().UTC().Add(-time.Hour),
|
|
)
|
|
if state == storagepkg.QuarantineStateFailed {
|
|
markGoCacheQuarantineFailed(t, store, entry.ID)
|
|
}
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
_, err := controller.Restore(context.Background(), entry.ID)
|
|
if !errors.Is(err, storagepkg.ErrConflict) {
|
|
t.Fatalf("Restore error = %v, want storage ErrConflict", err)
|
|
}
|
|
stored, err := store.GetQuarantineEntry(context.Background(), entry.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.State != state {
|
|
t.Fatalf("state = %q, want unchanged %q", stored.State, state)
|
|
}
|
|
if data, err := os.ReadFile(artifact); err != nil || string(data) != "active cache" {
|
|
t.Fatalf("replacement cache changed: data=%q err=%v", data, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerDoesNotMarkMissingPayloadPlaceholderRestored(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
current, err := settings.GetSettings(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
current.GoCache.Enabled = true
|
|
if _, err := settings.SaveSettings(context.Background(), current); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
provider := gocache.New(gocache.Config{
|
|
HomeDir: home, TrashDir: filepath.Join(home, "trash"), Settings: settings,
|
|
})
|
|
if _, err := provider.ExecutionEnvironment(context.Background()); err != nil {
|
|
t.Fatalf("create managed replacement: %v", err)
|
|
}
|
|
entry := createGoCacheQuarantineEntry(
|
|
t, store, original, quarantined, time.Now().UTC().Add(time.Hour),
|
|
)
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
_, err = controller.Restore(context.Background(), entry.ID)
|
|
if !errors.Is(err, storagepkg.ErrConflict) {
|
|
t.Fatalf("Restore error = %v, want storage ErrConflict", err)
|
|
}
|
|
stored, err := store.GetQuarantineEntry(context.Background(), entry.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.State != storagepkg.QuarantineStateQuarantined {
|
|
t.Fatalf("state = %q, want quarantined", stored.State)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerReportsPersistenceAndFailsClosedAfterRollbackFailure(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entry := storagepkg.QuarantineEntry{
|
|
ID: "entry-cache", ResourceType: storagepkg.ResourceTypeGoCache,
|
|
OriginalPath: original, QuarantinePath: quarantined,
|
|
State: storagepkg.QuarantineStateQuarantined, Metadata: []byte(`{"ownership":"managed"}`),
|
|
}
|
|
store := &failingTransitionQuarantineStore{entry: entry, err: errors.New("database unavailable")}
|
|
renameCalls := 0
|
|
controller := &workspaceQuarantineController{
|
|
store: store, homeDir: home,
|
|
rename: func(oldPath, newPath string) error {
|
|
renameCalls++
|
|
if renameCalls == 2 {
|
|
return errors.New("rollback blocked")
|
|
}
|
|
return os.Rename(oldPath, newPath)
|
|
},
|
|
}
|
|
|
|
_, err := controller.Restore(context.Background(), entry.ID)
|
|
if err == nil || !strings.Contains(err.Error(), "database unavailable") ||
|
|
!strings.Contains(err.Error(), "rollback blocked") {
|
|
t.Fatalf("Restore error = %v, want persistence and rollback failures", err)
|
|
}
|
|
if _, err := os.Stat(original); err != nil {
|
|
t.Fatalf("restored data missing after failed rollback: %v", err)
|
|
}
|
|
if _, err := os.Stat(quarantined); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("quarantine path exists after failed rollback: %v", err)
|
|
}
|
|
|
|
store.err = nil
|
|
_, err = controller.Restore(context.Background(), entry.ID)
|
|
if !errors.Is(err, storagepkg.ErrConflict) {
|
|
t.Fatalf("ambiguous retry error = %v, want storage ErrConflict", err)
|
|
}
|
|
if store.entry.State != storagepkg.QuarantineStateQuarantined {
|
|
t.Fatalf("retry state = %q, want quarantined", store.entry.State)
|
|
}
|
|
}
|
|
|
|
type failingTransitionQuarantineStore struct {
|
|
entry storagepkg.QuarantineEntry
|
|
err error
|
|
}
|
|
|
|
type removePayloadOnGetStore struct {
|
|
delegate *storagepkg.Store
|
|
path string
|
|
removed bool
|
|
}
|
|
|
|
func (s *removePayloadOnGetStore) GetQuarantineEntry(
|
|
ctx context.Context,
|
|
id string,
|
|
) (storagepkg.QuarantineEntry, error) {
|
|
entry, err := s.delegate.GetQuarantineEntry(ctx, id)
|
|
if err != nil || s.removed {
|
|
return entry, err
|
|
}
|
|
if err := os.RemoveAll(s.path); err != nil {
|
|
return storagepkg.QuarantineEntry{}, err
|
|
}
|
|
s.removed = true
|
|
return entry, nil
|
|
}
|
|
|
|
func (s *removePayloadOnGetStore) ListQuarantineEntries(
|
|
ctx context.Context,
|
|
includeTerminal bool,
|
|
) ([]storagepkg.QuarantineEntry, error) {
|
|
return s.delegate.ListQuarantineEntries(ctx, includeTerminal)
|
|
}
|
|
|
|
func (s *removePayloadOnGetStore) TransitionQuarantineEntry(
|
|
ctx context.Context,
|
|
id string,
|
|
next storagepkg.QuarantineState,
|
|
lastError string,
|
|
) (storagepkg.QuarantineEntry, error) {
|
|
return s.delegate.TransitionQuarantineEntry(ctx, id, next, lastError)
|
|
}
|
|
|
|
func (s *failingTransitionQuarantineStore) GetQuarantineEntry(
|
|
context.Context, string,
|
|
) (storagepkg.QuarantineEntry, error) {
|
|
return s.entry, nil
|
|
}
|
|
|
|
func (s *failingTransitionQuarantineStore) ListQuarantineEntries(
|
|
context.Context, bool,
|
|
) ([]storagepkg.QuarantineEntry, error) {
|
|
return []storagepkg.QuarantineEntry{s.entry}, nil
|
|
}
|
|
|
|
func (s *failingTransitionQuarantineStore) TransitionQuarantineEntry(
|
|
_ context.Context,
|
|
_ string,
|
|
next storagepkg.QuarantineState,
|
|
lastError string,
|
|
) (storagepkg.QuarantineEntry, error) {
|
|
if s.err != nil {
|
|
return storagepkg.QuarantineEntry{}, s.err
|
|
}
|
|
s.entry.State = next
|
|
s.entry.LastError = lastError
|
|
return s.entry, nil
|
|
}
|
|
|
|
func TestQuarantineControllerFailedGoCacheRetryRejectsSymlinkedRestorePath(t *testing.T) {
|
|
root := t.TempDir()
|
|
home := filepath.Join(root, "home")
|
|
external := filepath.Join(root, "external")
|
|
if err := os.MkdirAll(filepath.Join(external, "go-build"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(home, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
linkedParent := filepath.Join(root, "linked-external")
|
|
if err := os.Symlink(external, linkedParent); err != nil {
|
|
t.Skipf("symlinks unavailable: %v", err)
|
|
}
|
|
original := filepath.Join(linkedParent, "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
if _, err := settings.AdoptGoCachePath(context.Background(), original); err != nil {
|
|
t.Fatalf("AdoptGoCachePath: %v", err)
|
|
}
|
|
entry := createGoCacheQuarantineEntry(
|
|
t, store, original, quarantined, time.Now().UTC().Add(time.Hour),
|
|
)
|
|
markGoCacheQuarantineFailed(t, store, entry.ID)
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
_, err := controller.Restore(context.Background(), entry.ID)
|
|
if !errors.Is(err, storagepkg.ErrValidation) {
|
|
t.Fatalf("Restore error = %v, want storage ErrValidation", err)
|
|
}
|
|
stored, err := store.GetQuarantineEntry(context.Background(), entry.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.State != storagepkg.QuarantineStateFailed {
|
|
t.Fatalf("state = %q, want failed", stored.State)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerPermanentlyDeletesGoCache(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := createGoCacheQuarantineEntry(t, store, original, quarantined, time.Now().UTC().Add(-time.Hour))
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
deleted, err := controller.PermanentDelete(context.Background(), entry.ID, "DELETE")
|
|
if err != nil {
|
|
t.Fatalf("PermanentDelete: %v", err)
|
|
}
|
|
if deleted.State != storagepkg.QuarantineStateDeleted {
|
|
t.Fatalf("state = %q, want deleted", deleted.State)
|
|
}
|
|
if _, err := os.Stat(quarantined); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("quarantine path still exists: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerPermanentlyDeletesMissingGoCachePayload(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
force bool
|
|
}{
|
|
{name: "eligible"},
|
|
{name: "forced", force: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(original, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
artifact := filepath.Join(original, "replacement-artifact")
|
|
if err := os.WriteFile(artifact, []byte("active cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
deleteAfter := time.Now().UTC().Add(-time.Hour)
|
|
if test.force {
|
|
deleteAfter = time.Now().UTC().Add(time.Hour)
|
|
}
|
|
entry := createGoCacheQuarantineEntry(t, store, original, quarantined, deleteAfter)
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
var deleted storagepkg.QuarantineEntry
|
|
var err error
|
|
if test.force {
|
|
deleted, err = controller.PermanentDeleteForce(
|
|
context.Background(), entry.ID, storagepkg.QuarantineConfirmationForce,
|
|
)
|
|
} else {
|
|
deleted, err = controller.PermanentDelete(
|
|
context.Background(), entry.ID, storagepkg.QuarantineConfirmationDelete,
|
|
)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("delete missing payload: %v", err)
|
|
}
|
|
if deleted.State != storagepkg.QuarantineStateDeleted {
|
|
t.Fatalf("state = %q, want deleted", deleted.State)
|
|
}
|
|
if data, err := os.ReadFile(artifact); err != nil || string(data) != "active cache" {
|
|
t.Fatalf("replacement cache changed: data=%q err=%v", data, err)
|
|
}
|
|
if _, err := os.Stat(quarantined); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("missing quarantine payload became present: %v", err)
|
|
}
|
|
stored, err := store.GetQuarantineEntry(context.Background(), entry.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.State != storagepkg.QuarantineStateDeleted {
|
|
t.Fatalf("stored state = %q, want deleted", stored.State)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerPurgeReportsZeroBytesForMissingGoCachePayload(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "missing-payload")
|
|
if err := os.MkdirAll(original, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
artifact := filepath.Join(original, "replacement-artifact")
|
|
if err := os.WriteFile(artifact, []byte("active cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := storagepkg.QuarantineEntry{
|
|
ID: "missing-payload",
|
|
ResourceType: storagepkg.ResourceTypeGoCache,
|
|
OriginalPath: original,
|
|
QuarantinePath: quarantined,
|
|
SizeBytes: 42,
|
|
State: storagepkg.QuarantineStateQuarantined,
|
|
QuarantinedAt: time.Now().UTC().Add(-2 * time.Hour),
|
|
DeleteAfter: time.Now().UTC().Add(-time.Hour),
|
|
Metadata: json.RawMessage(`{"ownership":"managed"}`),
|
|
}
|
|
if err := store.CreateQuarantineEntry(context.Background(), &entry); err != nil {
|
|
t.Fatalf("create missing-payload entry: %v", err)
|
|
}
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
result, err := controller.Purge(
|
|
context.Background(), storagepkg.QuarantinePurgeScopeEligible,
|
|
storagepkg.QuarantineConfirmationEligible,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Purge: %v", err)
|
|
}
|
|
if result.Deleted != 1 || result.Failed != 0 || result.DeletedBytes != 0 {
|
|
t.Fatalf("purge result = %#v, want one deleted entry, zero failures, zero deleted bytes", result)
|
|
}
|
|
if data, err := os.ReadFile(artifact); err != nil || string(data) != "active cache" {
|
|
t.Fatalf("replacement cache changed: data=%q err=%v", data, err)
|
|
}
|
|
stored, err := store.GetQuarantineEntry(context.Background(), entry.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.State != storagepkg.QuarantineStateDeleted {
|
|
t.Fatalf("stored state = %q, want deleted", stored.State)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerPurgeReportsBytesFromDeletionOutcome(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "removed-before-delete")
|
|
if err := os.MkdirAll(original, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
liveArtifact := filepath.Join(original, "replacement-artifact")
|
|
if err := os.WriteFile(liveArtifact, []byte("active cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(quarantined, "old-artifact"), []byte("old cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := storagepkg.QuarantineEntry{
|
|
ID: "removed-before-delete",
|
|
ResourceType: storagepkg.ResourceTypeGoCache,
|
|
OriginalPath: original,
|
|
QuarantinePath: quarantined,
|
|
SizeBytes: 42,
|
|
State: storagepkg.QuarantineStateQuarantined,
|
|
QuarantinedAt: time.Now().UTC().Add(-2 * time.Hour),
|
|
DeleteAfter: time.Now().UTC().Add(-time.Hour),
|
|
Metadata: json.RawMessage(`{"ownership":"managed"}`),
|
|
}
|
|
if err := store.CreateQuarantineEntry(context.Background(), &entry); err != nil {
|
|
t.Fatalf("create entry: %v", err)
|
|
}
|
|
deletingStore := &removePayloadOnGetStore{delegate: store, path: quarantined}
|
|
controller := &workspaceQuarantineController{settings: settings, store: deletingStore, homeDir: home}
|
|
|
|
result, err := controller.Purge(
|
|
context.Background(), storagepkg.QuarantinePurgeScopeEligible,
|
|
storagepkg.QuarantineConfirmationEligible,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Purge: %v", err)
|
|
}
|
|
if result.Deleted != 1 || result.Failed != 0 || result.DeletedBytes != 0 {
|
|
t.Fatalf("purge result = %#v, want one deleted entry and zero deleted bytes", result)
|
|
}
|
|
if data, err := os.ReadFile(liveArtifact); err != nil || string(data) != "active cache" {
|
|
t.Fatalf("live cache changed: data=%q err=%v", data, err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerDeletesHistoricalAdoptedGoCacheAfterReadoption(t *testing.T) {
|
|
root := t.TempDir()
|
|
home := filepath.Join(root, "home")
|
|
original := filepath.Join(root, "first-cache")
|
|
current := filepath.Join(root, "second-cache")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
if _, err := settings.AdoptGoCachePath(context.Background(), current); err != nil {
|
|
t.Fatalf("AdoptGoCachePath: %v", err)
|
|
}
|
|
entry := storagepkg.QuarantineEntry{
|
|
ID: "entry-cache", ResourceType: storagepkg.ResourceTypeGoCache,
|
|
OriginalPath: original, QuarantinePath: quarantined,
|
|
State: storagepkg.QuarantineStateQuarantined,
|
|
QuarantinedAt: time.Now().UTC().Add(-2 * time.Hour),
|
|
DeleteAfter: time.Now().UTC().Add(-time.Hour),
|
|
Metadata: json.RawMessage(`{"ownership":"adopted"}`),
|
|
}
|
|
if err := store.CreateQuarantineEntry(context.Background(), &entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
deleted, err := controller.PermanentDelete(context.Background(), entry.ID, "DELETE")
|
|
if err != nil {
|
|
t.Fatalf("PermanentDelete: %v", err)
|
|
}
|
|
if deleted.State != storagepkg.QuarantineStateDeleted {
|
|
t.Fatalf("state = %q, want deleted", deleted.State)
|
|
}
|
|
if _, err := os.Stat(quarantined); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("historical quarantine path still exists: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestQuarantineControllerRejectsEarlyGoCacheDeleteAndKeepsQuarantine(t *testing.T) {
|
|
home := t.TempDir()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", "entry-cache")
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, store := newStorageMaintenanceStores(t)
|
|
entry := createGoCacheQuarantineEntry(
|
|
t, store, original, quarantined, time.Now().UTC().Add(time.Hour),
|
|
)
|
|
controller := &workspaceQuarantineController{settings: settings, store: store, homeDir: home}
|
|
|
|
_, err := controller.PermanentDelete(context.Background(), entry.ID, "DELETE")
|
|
if !errors.Is(err, storagepkg.ErrConflict) {
|
|
t.Fatalf("PermanentDelete error = %v, want storage ErrConflict", err)
|
|
}
|
|
if _, err := os.Stat(quarantined); err != nil {
|
|
t.Fatalf("quarantine path changed before deadline: %v", err)
|
|
}
|
|
got, err := store.GetQuarantineEntry(context.Background(), entry.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.State != storagepkg.QuarantineStateQuarantined {
|
|
t.Fatalf("quarantine state = %q, want quarantined", got.State)
|
|
}
|
|
}
|
|
|
|
func createGoCacheQuarantineEntry(
|
|
t *testing.T,
|
|
store *storagepkg.Store,
|
|
original string,
|
|
quarantined string,
|
|
deleteAfter time.Time,
|
|
) storagepkg.QuarantineEntry {
|
|
t.Helper()
|
|
homeDir := filepath.Dir(filepath.Dir(filepath.Dir(quarantined)))
|
|
ownership := "adopted"
|
|
if filepath.Clean(original) == filepath.Join(homeDir, "cache", "go-build") {
|
|
ownership = "managed"
|
|
}
|
|
metadata, err := json.Marshal(map[string]string{"ownership": ownership})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entry := storagepkg.QuarantineEntry{
|
|
ID: "entry-cache", ResourceType: storagepkg.ResourceTypeGoCache,
|
|
OriginalPath: original, QuarantinePath: quarantined,
|
|
State: storagepkg.QuarantineStateQuarantined,
|
|
QuarantinedAt: time.Now().UTC().Add(-2 * time.Hour), DeleteAfter: deleteAfter,
|
|
Metadata: metadata,
|
|
}
|
|
if err := store.CreateQuarantineEntry(context.Background(), &entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func createGoCacheQuarantineEntryWithID(
|
|
t *testing.T,
|
|
store *storagepkg.Store,
|
|
home string,
|
|
id string,
|
|
deleteAfter time.Time,
|
|
) storagepkg.QuarantineEntry {
|
|
t.Helper()
|
|
original := filepath.Join(home, "cache", "go-build")
|
|
quarantined := filepath.Join(home, "trash", "go-cache", id)
|
|
if err := os.MkdirAll(quarantined, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(quarantined, "artifact"), []byte("cache"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ownership := "managed"
|
|
metadata, err := json.Marshal(map[string]string{"ownership": ownership})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entry := storagepkg.QuarantineEntry{
|
|
ID: id, ResourceType: storagepkg.ResourceTypeGoCache,
|
|
OriginalPath: original, QuarantinePath: quarantined,
|
|
State: storagepkg.QuarantineStateQuarantined,
|
|
QuarantinedAt: time.Now().UTC().Add(-2 * time.Hour), DeleteAfter: deleteAfter,
|
|
Metadata: metadata,
|
|
}
|
|
if err := store.CreateQuarantineEntry(context.Background(), &entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func markGoCacheQuarantineFailed(t *testing.T, store *storagepkg.Store, id string) {
|
|
t.Helper()
|
|
if _, err := store.TransitionQuarantineEntry(
|
|
context.Background(), id, storagepkg.QuarantineStateFailed, "retryable failure",
|
|
); err != nil {
|
|
t.Fatalf("mark quarantine failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func newStorageMaintenanceStores(t *testing.T) (*storagepkg.SettingsStore, *storagepkg.Store) {
|
|
t.Helper()
|
|
connection, err := sqlx.Open("sqlite3", filepath.Join(t.TempDir(), "storage.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection.SetMaxOpenConns(1)
|
|
pool := db.NewPool(connection, connection)
|
|
t.Cleanup(func() { _ = pool.Close() })
|
|
rawSettings, err := systemsettings.NewStore(pool)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store, err := storagepkg.NewStore(pool)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return storagepkg.NewSettingsStore(rawSettings), store
|
|
}
|