@@ -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.
2440 ReadmeFile = "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 {
3451 return 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
82103 return 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.
85108 reader := tar .NewReader (bytes .NewBuffer (cfg .TemplateSourceArchive ))
86109 // for safety, nil out the reference on Config, since the reader now owns it.
87110 cfg .TemplateSourceArchive = nil
@@ -190,3 +213,40 @@ func (l Layout) Cleanup(ctx context.Context, logger slog.Logger, fs afero.Fs) {
190213 logger .Error (ctx , "failed to clean up work directory after multiple attempts" ,
191214 slog .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+ }
0 commit comments