@@ -14,6 +14,7 @@ import (
1414 "github.com/open-source-cloud/devstack/internal/generate"
1515 "github.com/open-source-cloud/devstack/internal/lock"
1616 "github.com/open-source-cloud/devstack/internal/orchestrate"
17+ "github.com/open-source-cloud/devstack/internal/resource"
1718)
1819
1920// newWorkspaceCmd wires `devstack workspace <sub>` — workspace-scoped lifecycle
@@ -36,7 +37,7 @@ func newWorkspaceCmd(g *GlobalOpts) *cobra.Command {
3637// PRESERVES data: named volumes and provisioned DBs/roles survive (shared
3738// per-service volume removal is `uninstall`/`db gc` territory).
3839func newWorkspaceDestroyCmd (g * GlobalOpts ) * cobra.Command {
39- var yes bool
40+ var yes , purgeData bool
4041 cmd := & cobra.Command {
4142 Use : "destroy" ,
4243 Short : "Tear down THIS workspace's stacks and release its refs/ports (volumes/DBs preserved)" ,
@@ -63,17 +64,21 @@ func newWorkspaceDestroyCmd(g *GlobalOpts) *cobra.Command {
6364
6465 projects := sortedProjectNames (d .Model )
6566 if ! yes {
67+ dataLine := "Volumes and databases are PRESERVED."
68+ if purgeData {
69+ dataLine = "WARNING: --purge-data DROPS every provisioned database/bucket/etc (DATA DESTROYED)."
70+ }
6671 prompt := fmt .Sprintf (
6772 "This tears down workspace %q (%d project stack(s)) and releases its refs/ports.\n " +
68- "Volumes and databases are PRESERVED. Type 'yes' to continue: " ,
69- d .Model .Workspace .Name , len (projects ))
73+ "%s Type 'yes' to continue: " ,
74+ d .Model .Workspace .Name , len (projects ), dataLine )
7075 if ! confirm (cmd , prompt ) {
7176 fmt .Fprintln (cmd .OutOrStdout (), "aborted" )
7277 return nil
7378 }
7479 }
7580
76- res := destroyWorkspace (cmd .Context (), d , projects )
81+ res := destroyWorkspace (cmd .Context (), d , projects , purgeData )
7782 if g .JSON {
7883 if err := writeJSON (cmd , res ); err != nil {
7984 return err
@@ -86,11 +91,18 @@ func newWorkspaceDestroyCmd(g *GlobalOpts) *cobra.Command {
8691 for _ , s := range res .SharedStopped {
8792 fmt .Fprintf (w , "[ok] stopped shared %s (0 refs)\n " , s )
8893 }
94+ for _ , p := range res .PurgedResources {
95+ fmt .Fprintf (w , "[ok] dropped %s %s\n " , p ["kind" ], p ["name" ])
96+ }
8997 for _ , e := range res .Errors {
9098 fmt .Fprintf (w , "[warn] %s\n " , e )
9199 }
92- fmt .Fprintf (w , "destroyed workspace %q: %d stack(s) down, %d shared stopped (volumes/DBs preserved)\n " ,
93- d .Model .Workspace .Name , len (res .Projects ), len (res .SharedStopped ))
100+ dataNote := "volumes/DBs preserved"
101+ if purgeData {
102+ dataNote = fmt .Sprintf ("%d resource(s) purged" , len (res .PurgedResources ))
103+ }
104+ fmt .Fprintf (w , "destroyed workspace %q: %d stack(s) down, %d shared stopped (%s)\n " ,
105+ d .Model .Workspace .Name , len (res .Projects ), len (res .SharedStopped ), dataNote )
94106 }
95107 if len (res .Errors ) > 0 {
96108 return fmt .Errorf ("destroy completed with %d error(s)" , len (res .Errors ))
@@ -99,28 +111,63 @@ func newWorkspaceDestroyCmd(g *GlobalOpts) *cobra.Command {
99111 },
100112 }
101113 cmd .Flags ().BoolVar (& yes , "yes" , false , "skip the confirmation prompt (required for --json/non-interactive)" )
114+ cmd .Flags ().BoolVar (& purgeData , "purge-data" , false , "also DROP every provisioned resource (databases/buckets/…) — DESTRUCTIVE" )
102115 return cmd
103116}
104117
105118// DestroyResult is the machine-readable outcome of `workspace destroy`.
106119type DestroyResult struct {
107- Workspace string `json:"workspace"`
108- Projects []string `json:"projects"` // project stacks brought down
109- SharedStopped []string `json:"shared_stopped"` // orphaned shared services warm-stopped
110- Errors []string `json:"errors,omitempty"`
120+ Workspace string `json:"workspace"`
121+ Projects []string `json:"projects"` // project stacks brought down
122+ SharedStopped []string `json:"shared_stopped"` // orphaned shared services warm-stopped
123+ PurgedResources []map [string ]string `json:"purged_resources,omitempty"` // --purge-data: resources dropped
124+ Errors []string `json:"errors,omitempty"`
111125}
112126
113127// destroyWorkspace performs the teardown mechanics (no prompting) so it is
114128// unit-testable with injected mocks. It is best-effort: a failure on one project
115129// is recorded and the rest proceed, so a partially-broken workspace can still be
116130// cleaned up.
117- func destroyWorkspace (ctx context.Context , d orchestrate.UpDeps , projects []string ) DestroyResult {
131+ func destroyWorkspace (ctx context.Context , d orchestrate.UpDeps , projects []string , purgeData bool ) DestroyResult {
118132 res := DestroyResult {Workspace : d .Model .Workspace .Name }
119133 runner := d .Runner
120134 if runner == nil {
121135 runner = docker.ExecRunner {}
122136 }
123137
138+ // 0. --purge-data (opt-in, DESTRUCTIVE): while the shared engine is still up,
139+ // DROP every resource this workspace provisioned, then remove its ledger rows.
140+ // The data-preserving default skips this entirely (spec 27).
141+ if purgeData {
142+ for _ , p := range projects {
143+ rows , err := d .DB .ProvisionedFor (p )
144+ if err != nil {
145+ res .Errors = append (res .Errors , fmt .Sprintf ("list resources for %s: %v" , p , err ))
146+ continue
147+ }
148+ for _ , row := range rows {
149+ if row .Kind == "role" {
150+ continue // dropped alongside its database
151+ }
152+ r := resource.Resource {Engine : engineForKindGuess (row .Kind ), Kind : row .Kind , Name : row .Name , Owner : p }
153+ if err := orchestrate .DropResource (ctx , d , r , true ); err != nil {
154+ res .Errors = append (res .Errors , fmt .Sprintf ("drop %s %s: %v" , row .Kind , row .Name , err ))
155+ continue
156+ }
157+ res .PurgedResources = append (res .PurgedResources , map [string ]string {"project" : p , "kind" : row .Kind , "name" : row .Name })
158+ }
159+ // Remove any straggler rows (e.g. redis_index) and the overlay ports.
160+ if err := lock .WithLock (ctx , d .LockPath , func () error {
161+ if _ , err := d .DB .RemoveProvisionedForProject (p ); err != nil {
162+ return err
163+ }
164+ return nil
165+ }); err != nil {
166+ res .Errors = append (res .Errors , fmt .Sprintf ("clear provisioned rows for %s: %v" , p , err ))
167+ }
168+ }
169+ }
170+
124171 // 1. compose down each project stack (containers + project default network;
125172 // named volumes survive — never -v here).
126173 for _ , p := range projects {
0 commit comments