@@ -19,11 +19,28 @@ import (
1919"github.com/coder/coder/v2/provisionersdk/proto"
2020)
2121
22+ type Layouter interface {
23+ WorkDirectory ()string
24+ StateFilePath ()string
25+ PlanFilePath ()string
26+ TerraformLockFile ()string
27+ ReadmeFilePath ()string
28+ TerraformMetadataDir ()string
29+ ModulesDirectory ()string
30+ ModulesFilePath ()string
31+ ExtractArchive (ctx context.Context ,logger slog.Logger ,fs afero.Fs ,cfg * proto.Config )error
32+ Cleanup (ctx context.Context ,logger slog.Logger ,fs afero.Fs )
33+ CleanStaleSessions (ctx context.Context ,logger slog.Logger ,fs afero.Fs ,now time.Time )error
34+ }
35+
36+ var _ Layouter = (* Layout )(nil )
37+
2238const (
2339// ReadmeFile is the location we look for to extract documentation from template versions.
2440ReadmeFile = "README.md"
2541
26- sessionDirPrefix = "Session"
42+ sessionDirPrefix = "Session"
43+ staleSessionRetention = 7 * 24 * time .Hour
2744)
2845
2946// Session creates a directory structure layout for terraform execution. The
@@ -34,6 +51,10 @@ func Session(parentDirPath, sessionID string) Layout {
3451return Layout (filepath .Join (parentDirPath ,sessionDirPrefix + sessionID ))
3552}
3653
54+ func FromWorkingDirectory (workDir string )Layout {
55+ return Layout (workDir )
56+ }
57+
3758// Layout is the terraform execution working directory structure.
3859// It also contains some methods for common file operations within that layout.
3960// Such as "Cleanup" and "ExtractArchive".
@@ -82,6 +103,8 @@ func (l Layout) ExtractArchive(ctx context.Context, logger slog.Logger, fs afero
82103return xerrors .Errorf ("create work directory %q: %w" ,l .WorkDirectory (),err )
83104}
84105
106+ // TODO: Pass in cfg.TemplateSourceArchive, not the full config.
107+ // niling out the config field is a bit hacky.
85108reader := tar .NewReader (bytes .NewBuffer (cfg .TemplateSourceArchive ))
86109// for safety, nil out the reference on Config, since the reader now owns it.
87110cfg .TemplateSourceArchive = nil
@@ -190,3 +213,40 @@ func (l Layout) Cleanup(ctx context.Context, logger slog.Logger, fs afero.Fs) {
190213logger .Error (ctx ,"failed to clean up work directory after multiple attempts" ,
191214slog .F ("path" ,path ),slog .Error (err ))
192215}
216+
217+ // CleanStaleSessions browses the work directory searching for stale session
218+ // directories. Coder provisioner is supposed to remove them once after finishing the provisioning,
219+ // but there is a risk of keeping them in case of a failure.
220+ func (l Layout )CleanStaleSessions (ctx context.Context ,logger slog.Logger ,fs afero.Fs ,now time.Time )error {
221+ parent := filepath .Dir (l .WorkDirectory ())
222+ entries ,err := afero .ReadDir (fs ,filepath .Dir (l .WorkDirectory ()))
223+ if err != nil {
224+ return xerrors .Errorf ("can't read %q directory" ,parent )
225+ }
226+
227+ for _ ,fi := range entries {
228+ dirName := fi .Name ()
229+
230+ if fi .IsDir ()&& isValidSessionDir (dirName ) {
231+ sessionDirPath := filepath .Join (parent ,dirName )
232+
233+ modTime := fi .ModTime ()// fallback to modTime if modTime is not available (afero)
234+
235+ if modTime .Add (staleSessionRetention ).After (now ) {
236+ continue
237+ }
238+
239+ logger .Info (ctx ,"remove stale session directory" ,slog .F ("session_path" ,sessionDirPath ))
240+ err = fs .RemoveAll (sessionDirPath )
241+ if err != nil {
242+ return xerrors .Errorf ("can't remove %q directory: %w" ,sessionDirPath ,err )
243+ }
244+ }
245+ }
246+ return nil
247+ }
248+
249+ func isValidSessionDir (dirName string )bool {
250+ match ,err := filepath .Match (sessionDirPrefix + "*" ,dirName )
251+ return err == nil && match
252+ }