77 "context"
88 "fmt"
99 "log/slog"
10+ "os"
11+ "path/filepath"
12+ "strings"
1013 "time"
1114
1215 "github.com/stacklok/propolis/net"
@@ -22,6 +25,8 @@ type VM struct {
2225 dataDir string
2326 rootfsPath string
2427 ports []PortForward
28+ cacheDir string
29+ removeAll func (string ) error
2530}
2631
2732// VMInfo contains status information about a VM.
@@ -80,14 +85,39 @@ func (vm *VM) Status(_ context.Context) (*VMInfo, error) {
8085}
8186
8287// Remove stops the VM and cleans up its rootfs and state.
88+ // If the image cache lives under the data dir, its contents are preserved.
8389func (vm * VM ) Remove (ctx context.Context ) error {
8490 if vm .proc .IsAlive () {
8591 if err := vm .Stop (ctx ); err != nil {
8692 return fmt .Errorf ("stop before remove: %w" , err )
8793 }
8894 }
89- // Note: we intentionally do NOT remove the image cache —
90- // only the VM-specific state and rootfs extraction.
95+ if vm .removeAll == nil {
96+ vm .removeAll = os .RemoveAll
97+ }
98+
99+ if vm .rootfsPath != "" && ! isWithin (vm .cacheDir , vm .rootfsPath ) {
100+ if err := vm .removeAll (vm .rootfsPath ); err != nil {
101+ return fmt .Errorf ("remove rootfs: %w" , err )
102+ }
103+ }
104+
105+ if vm .dataDir != "" {
106+ var keep []string
107+ if vm .cacheDir != "" && isWithin (vm .dataDir , vm .cacheDir ) {
108+ keep = append (keep , vm .cacheDir )
109+ }
110+ if len (keep ) > 0 {
111+ if err := removeDataDirContentsExcept (vm .removeAll , vm .dataDir , keep ); err != nil {
112+ return fmt .Errorf ("remove data dir contents: %w" , err )
113+ }
114+ } else {
115+ if err := vm .removeAll (vm .dataDir ); err != nil {
116+ return fmt .Errorf ("remove data dir: %w" , err )
117+ }
118+ }
119+ }
120+
91121 return nil
92122}
93123
@@ -105,3 +135,44 @@ func (vm *VM) RootFSPath() string { return vm.rootfsPath }
105135
106136// Ports returns the configured port forwards.
107137func (vm * VM ) Ports () []PortForward { return vm .ports }
138+
139+ func isWithin (base string , target string ) bool {
140+ if base == "" || target == "" {
141+ return false
142+ }
143+ rel , err := filepath .Rel (base , target )
144+ if err != nil {
145+ return false
146+ }
147+ if rel == "." {
148+ return true
149+ }
150+ if rel == ".." {
151+ return false
152+ }
153+ return ! strings .HasPrefix (rel , ".." + string (filepath .Separator ))
154+ }
155+
156+ func removeDataDirContentsExcept (removeAll func (string ) error , dataDir string , keepPaths []string ) error {
157+ entries , err := os .ReadDir (dataDir )
158+ if err != nil {
159+ return fmt .Errorf ("read data dir: %w" , err )
160+ }
161+ keep := make (map [string ]struct {}, len (keepPaths ))
162+ for _ , path := range keepPaths {
163+ if path == "" {
164+ continue
165+ }
166+ keep [filepath .Clean (path )] = struct {}{}
167+ }
168+ for _ , entry := range entries {
169+ entryPath := filepath .Join (dataDir , entry .Name ())
170+ if _ , ok := keep [filepath .Clean (entryPath )]; ok {
171+ continue
172+ }
173+ if err := removeAll (entryPath ); err != nil {
174+ return fmt .Errorf ("remove data dir entry %s: %w" , entryPath , err )
175+ }
176+ }
177+ return nil
178+ }
0 commit comments