8000 feat: clean stale provisioner files by mtojek · Pull Request #9545 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: clean stale provisioner files #9545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
move to cleanups
  • Loading branch information
mtojek committed Sep 7, 2023
commit 574f0843f0b0a2ebea9ceb4171e1aceb60c6aa92
139 changes: 139 additions & 0 deletions provisioner/terraform/cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package terraform

import (
"context"
"io/fs"
"os"
"path/filepath"
"strings"
"time"

"cdr.dev/slog"
"github.com/djherbis/times"
"golang.org/x/xerrors"
)

// cleanStaleTerraformPlugins browses the Terraform cache directory
// and remove stale plugins that haven't been used for a while.
//
// Additionally, it sweeps empty, old directory trees.
//
// Sample cachePath: /Users/<username>/Library/Caches/coder/provisioner-<N>/tf
func cleanStaleTerraformPlugins(ctx context.Context, cachePath string, now time.Time, logger slog.Logger) error {
cachePath, err := filepath.Abs(cachePath) // sanity check in case the path is e.g. ../../../cache
if err != nil {
return xerrors.Errorf("unable to determine absolute path %q: %w", cachePath, err)
}

logger.Info(ctx, "clean stale Terraform plugins", slog.F("cache_path", cachePath))

// Filter directory trees matching pattern: <repositoryURL>/<company>/<plugin>/<version>/<distribution>
filterFunc := func(path string, info os.FileInfo) bool {
if !info.IsDir() {
return false
}

relativePath, err := filepath.Rel(cachePath, path)
if err != nil {
logger.Error(ctx, "unable to evaluate a relative path", slog.F("base", cachePath), slog.F("target", path), slog.Error(err))
return false
}

parts := strings.Split(relativePath, string(filepath.Separator))
return len(parts) == 5
}

// Review cached Terraform plugins
var pluginPaths []string
err = filepath.Walk(cachePath, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}

if !filterFunc(path, info) {
return nil
}

logger.Debug(ctx, "plugin directory discovered", slog.F("path", path))
pluginPaths = append(pluginPaths, path)
return nil
})
if err != nil {
return xerrors.Errorf("unable to walk through cache directory %q: %w", cachePath, err)
}

// Identify stale plugins
var stalePlugins []string
for _, pluginPath := range pluginPaths {
accessTime, err := latestAccessTime(pluginPath)
if err != nil {
return xerrors.Errorf("unable to evaluate latest access time for directory %q: %w", pluginPath, err)
}

if accessTime.Add(staleTerraformPluginRetention).Before(now) {
logger.Info(ctx, "plugin directory is stale and will be removed", slog.F("plugin_path", pluginPath))
stalePlugins = append(stalePlugins, pluginPath)
} else {
logger.Debug(ctx, "plugin directory is not stale", slog.F("plugin_path", pluginPath))
}
}

// Remove stale plugins
for _, stalePluginPath := range stalePlugins {
// Remove the plugin directory
err = os.RemoveAll(stalePluginPath)
if err != nil {
return xerrors.Errorf("unable to remove stale plugin %q: %w", stalePluginPath, err)
}

// Compact the plugin structure by removing empty directories.
wd := stalePluginPath
level := 4 // <repositoryURL>/<company>/<plugin>/<version>/<distribution>
for {
level--
if level == 0 {
break // do not compact further
}

wd = filepath.Dir(wd)

files, err := os.ReadDir(wd)
if err != nil {
return xerrors.Errorf("unable to read directory content %q: %w", wd, err)
}

if len(files) > 0 {
break // there are still other plugins
}

logger.Debug(ctx, "remove empty directory", slog.F("path", wd))
err = os.Remove(wd)
if err != nil {
return xerrors.Errorf("unable to remove directory %q: %w", wd, err)
}
}
}
return nil
}

// latestAccessTime walks recursively through the directory content, and locates
// the last accessed file.
func latestAccessTime(pluginPath string) (time.Time, error) {
var latest time.Time
err := filepath.Walk(pluginPath, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}

timeSpec := times.Get(info)
accessTime := timeSpec.AccessTime()
if latest.Before(accessTime) {
latest = accessTime
}
return nil
})
if err != nil {
return time.Time{}, xerrors.Errorf("unable to walk the plugin path %q: %w", pluginPath, err)
}
return latest, nil
}
132 changes: 1 addition & 131 deletions provisioner/terraform/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,16 @@ package terraform
import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"time"

"github.com/djherbis/times"
"golang.org/x/xerrors"

"cdr.dev/slog"
"github.com/coder/terraform-provider-coder/provider"

"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/terraform-provider-coder/provider"
)

const staleTerraformPluginRetention = 30 * 24 * time.Hour
Expand Down Expand Up @@ -251,128 +246,3 @@ func logTerraformEnvVars(sink logSink) {
}
}
}

// cleanStaleTerraformPlugins browses the Terraform cache directory
// and remove stale plugins that haven't been used for a while.
//
// Additionally, it sweeps empty, old directory trees.
//
// Sample cachePath: /Users/<username>/Library/Caches/coder/provisioner-<N>/tf
func cleanStaleTerraformPlugins(ctx context.Context, cachePath string, now time.Time, logger slog.Logger) error {
cachePath, err := filepath.Abs(cachePath) // sanity check in case the path is e.g. ../../../cache
if err != nil {
return xerrors.Errorf("unable to determine absolute path %q: %w", cachePath, err)
}

logger.Info(ctx, "clean stale Terraform plugins", slog.F("cache_path", cachePath))

// Filter directory trees matching pattern: <repositoryURL>/<company>/<plugin>/<version>/<distribution>
filterFunc := func(path string, info os.FileInfo) bool {
if !info.IsDir() {
return false
}

relativePath, err := filepath.Rel(cachePath, path)
if err != nil {
logger.Error(ctx, "unable to evaluate a relative path", slog.F("base", cachePath), slog. EDBE F("target", path), slog.Error(err))
return false
}

parts := strings.Split(relativePath, string(filepath.Separator))
return len(parts) == 5
}

// Review cached Terraform plugins
var pluginPaths []string
err = filepath.Walk(cachePath, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}

if !filterFunc(path, info) {
return nil
}

logger.Debug(ctx, "plugin directory discovered", slog.F("path", path))
pluginPaths = append(pluginPaths, path)
return nil
})
if err != nil {
return xerrors.Errorf("unable to walk through cache directory %q: %w", cachePath, err)
}

// Identify stale plugins
var stalePlugins []string
for _, pluginPath := range pluginPaths {
accessTime, err := latestAccessTime(pluginPath)
if err != nil {
return xerrors.Errorf("unable to evaluate latest access time for directory %q: %w", pluginPath, err)
}

if accessTime.Add(staleTerraformPluginRetention).Before(now) {
logger.Info(ctx, "plugin directory is stale and will be removed", slog.F("plugin_path", pluginPath))
stalePlugins = append(stalePlugins, pluginPath)
} else {
logger.Debug(ctx, "plugin directory is not stale", slog.F("plugin_path", pluginPath))
}
}

// Remove stale plugins
for _, stalePluginPath := range stalePlugins {
// Remove the plugin directory
err = os.RemoveAll(stalePluginPath)
if err != nil {
return xerrors.Errorf("unable to remove stale plugin %q: %w", stalePluginPath, err)
}

// Compact the plugin structure by removing empty directories.
wd := stalePluginPath
level := 4 // <repositoryURL>/<company>/<plugin>/<version>/<distribution>
for {
level--
if level == 0 {
break // do not compact further
}

wd = filepath.Dir(wd)

files, err := os.ReadDir(wd)
if err != nil {
return xerrors.Errorf("unable to read directory content %q: %w", wd, err)
}

if len(files) > 0 {
break // there are still other plugins
}

logger.Debug(ctx, "remove empty directory", slog.F("path", wd))
err = os.Remove(wd)
if err != nil {
return xerrors.Errorf("unable to remove directory %q: %w", wd, err)
}
}
}
return nil
}

// latestAccessTime walks recursively through the directory content, and locates
// the last accessed file.
func latestAccessTime(pluginPath string) (time.Time, error) {
var latest time.Time
err := filepath.Walk(pluginPath, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}

timeSpec := times.Get(info)
accessTime := timeSpec.AccessTime()
if latest.Before(accessTime) {
latest = accessTime
}
return nil
})
if err != nil {
return time.Time{}, xerrors.Errorf("unable to walk the plugin path %q: %w", pluginPath, err)
}
return latest, nil
}
52 changes: 52 additions & 0 deletions provisionersdk/cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package provisionersdk

import (
"context"
"os"
"path/filepath"
"time"

"github.com/djherbis/times"
"golang.org/x/xerrors"

"cdr.dev/slog"
)

// cleanStaleSessions browses the work directory searching for stale session
// directories. Coder provisioner is supposed to remove them once after finishing the provisioning,
// but there is a risk of keeping them in case of a failure.
func cleanStaleSessions(ctx context.Context, workDirectory string, now time.Time, logger slog.Logger) error {
entries, err := os.ReadDir(workDirectory)
if err != nil {
return xerrors.Errorf("can't read %q directory", workDirectory)
}

for _, entry := range entries {
dirName := entry.Name()

if entry.IsDir() && isValidSessionDir(dirName) {
sessionDirPath := filepath.Join(workDirectory, dirName)
fi, err := entry.Info()
if err != nil {
return xerrors.Errorf("can't read %q directory info: %w", sessionDirPath, err)
}

timeSpec := times.Get(fi)
if timeSpec.AccessTime().Add(staleSessionRetention).After(now) {
continue
}

logger.Info(ctx, "remove stale session directory: %s", sessionDirPath)
err = os.RemoveAll(sessionDirPath)
if err != nil {
return xerrors.Errorf("can't remove %q directory: %w", sessionDirPath, err)
}
}
}
return nil
}

func isValidSessionDir(dirName string) bool {
match, err := filepath.Match(sessionDirPrefix+"*", dirName)
return err == nil && match
}
Loading
0