Skip to content
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

Partial Implementation of project delete --force #850

Closed
wants to merge 7 commits into from
12 changes: 9 additions & 3 deletions client/incus_projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,20 @@ func (r *ProtocolIncus) RenameProject(name string, project api.ProjectPost) (Ope
return op, nil
}

// DeleteProject deletes a project.
func (r *ProtocolIncus) DeleteProject(name string) error {
// DeleteProject deletes a project gracefully or not,
// depending on the force flag).
func (r *ProtocolIncus) DeleteProject(name string, force bool) error {
if !r.HasExtension("projects") {
return fmt.Errorf("The server is missing the required \"projects\" API extension")
}

params := ""
if force {
params += "?force=1"
}

// Send the request
_, _, err := r.query("DELETE", fmt.Sprintf("/projects/%s", url.PathEscape(name)), nil, "")
_, _, err := r.query("DELETE", fmt.Sprintf("/projects/%s/%s", url.PathEscape(name), params), nil, "")
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ type InstanceServer interface {
CreateProject(project api.ProjectsPost) (err error)
UpdateProject(name string, project api.ProjectPut, ETag string) (err error)
RenameProject(name string, project api.ProjectPost) (op Operation, err error)
DeleteProject(name string) (err error)
DeleteProject(name string, force bool) (err error)

// Storage pool functions ("storage" API extension)
GetStoragePoolNames() (names []string, err error)
Expand Down
2 changes: 1 addition & 1 deletion cmd/incus-user/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ func serverSetupUser(uid uint32) error {
return fmt.Errorf("Unable to create project: %w", err)
}

revert.Add(func() { _ = client.DeleteProject(projectName) })
revert.Add(func() { _ = client.DeleteProject(projectName, true) })
}

// Parse the certificate.
Expand Down
33 changes: 29 additions & 4 deletions cmd/incus/project.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bufio"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -185,6 +186,7 @@ func (c *cmdProjectCreate) Run(cmd *cobra.Command, args []string) error {
type cmdProjectDelete struct {
global *cmdGlobal
project *cmdProject
flagForce bool
}

func (c *cmdProjectDelete) Command() *cobra.Command {
Expand All @@ -193,8 +195,9 @@ func (c *cmdProjectDelete) Command() *cobra.Command {
cmd.Aliases = []string{"rm"}
cmd.Short = i18n.G("Delete projects")
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
`Delete projects`))
`Delete projects. Use flag -f to force delete a project.`))

cmd.Flags().BoolVarP(&c.flagForce, "force", "f", false, i18n.G("Force delete the project and all its attributes."))
cmd.RunE = c.Run

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
Expand All @@ -208,6 +211,20 @@ func (c *cmdProjectDelete) Command() *cobra.Command {
return cmd
}

func (c *cmdProjectDelete) promptConfirmation(name string) error {
reader := bufio.NewReader(os.Stdin)
fmt.Printf(i18n.G(`Careful, force removal may not be properly implemented.
This message is a placeholder. Are you really sure you want to force removing %s? (yes/no): `), name)
input, _ := reader.ReadString('\n')
input = strings.TrimSuffix(input, "\n")

if !slices.Contains([]string{i18n.G("yes")}, strings.ToLower(input)) {
return fmt.Errorf(i18n.G("User aborted delete operation"))
}

return nil
}

func (c *cmdProjectDelete) Run(cmd *cobra.Command, args []string) error {
// Quick checks.
exit, err := c.global.CheckArgs(cmd, args, 1, 1)
Expand All @@ -228,13 +245,21 @@ func (c *cmdProjectDelete) Run(cmd *cobra.Command, args []string) error {

resource := resources[0]

// Prompt for confirmation if --force is used.
if c.flagForce {
err := c.promptConfirmation(resource.name)
if err != nil {
return err
}
}

if resource.name == "" {
return fmt.Errorf(i18n.G("Missing project name"))
}

// Delete the project
err = resource.server.DeleteProject(resource.name)
if err != nil {
// Delete the project, server is unable to find the project here.
err = resource.server.DeleteProject(resource.name, c.flagForce)
if err != nil {
return err
}

Expand Down
39 changes: 28 additions & 11 deletions cmd/incusd/api_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"slices"
"strconv"
"strings"

"github.com/gorilla/mux"
Expand Down Expand Up @@ -877,6 +878,11 @@ func projectPost(d *Daemon, r *http.Request) response.Response {
func projectDelete(d *Daemon, r *http.Request) response.Response {
s := d.State()

force, err := strconv.Atoi(r.FormValue("force"))
if err != nil {
force = 0
}

name, err := url.PathUnescape(mux.Vars(r)["name"])
if err != nil {
return response.SmartError(err)
Expand All @@ -894,21 +900,32 @@ func projectDelete(d *Daemon, r *http.Request) response.Response {
return fmt.Errorf("Fetch project %q: %w", name, err)
}

empty, err := projectIsEmpty(ctx, project, tx)
if err != nil {
return err
}
if force==1 {
//project force delete untested method
//todo: call the incus daemon and use it to DeleteInstance, DeleteProfile, UpdateProfile, etc.
//unsure how to access the UsedBy field via GetProject call.


if !empty {
return fmt.Errorf("Only empty projects can be removed")
}
//default delete behavior, may change when force delete is implemented
if force==0 {
empty, err := projectIsEmpty(ctx, project, tx)
if err != nil {
return err
}

id, err = cluster.GetProjectID(ctx, tx.Tx(), name)
if err != nil {
return fmt.Errorf("Fetch project id %q: %w", name, err)
}
if !empty {
return fmt.Errorf("Only empty projects can be removed")
}

id, err = cluster.GetProjectID(ctx, tx.Tx(), name)
if err != nil {
return fmt.Errorf("Fetch project id %q: %w", name, err)
}

return cluster.DeleteProject(ctx, tx.Tx(), name)
return cluster.DeleteProject(ctx, tx.Tx(), name)
}
return err
})

if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,13 @@ Add a new project API, supporting creation, update and deletion of projects.
Projects can hold containers, profiles or images at this point and let
you get a separate view of your Incus resources by switching to it.

## `projects_force_delete`

This allows force deletion of projects and related artifacts.

This will remove any instances, volumes and buckets, profiles and networks
(starting with acls, zones, peering before networks themselves).

## `network_vxlan_ttl`

This adds a new `tunnel.NAME.ttl` network configuration option which
Expand Down
1 change: 1 addition & 0 deletions internal/version/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ var APIExtensions = []string{
"storage_api_volume_snapshots",
"storage_unmapped",
"projects",
"projects_force_delete",
"network_vxlan_ttl",
"container_incremental_copy",
"usb_optional_vendorid",
Expand Down
Loading