Files
photoprism/internal/commands/cluster_nodes_remove.go
2025-10-21 16:51:24 +02:00

150 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package commands
import (
"context"
"fmt"
"time"
"github.com/manifoldco/promptui"
"github.com/urfave/cli/v2"
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service/cluster/provisioner"
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/log/status"
)
// ClusterNodesRemoveCommand deletes a node from the registry.
var ClusterNodesRemoveCommand = &cli.Command{
Name: "rm",
Usage: "Deletes a node from the registry",
ArgsUsage: "<id|name>",
Flags: []cli.Flag{
DryRunFlag("preview deletion without modifying the registry or database"),
&cli.BoolFlag{Name: "drop-db", Aliases: []string{"d"}, Usage: "drop the nodes provisioned database and user after registry deletion"},
&cli.BoolFlag{Name: "all-ids", Usage: "delete all records that share the same UUID (admin cleanup)"},
YesFlag(),
},
Hidden: true, // Required for cluster-management only.
Action: clusterNodesRemoveAction,
}
func clusterNodesRemoveAction(ctx *cli.Context) error {
return CallWithDependencies(ctx, func(conf *config.Config) error {
if !conf.Portal() {
return cli.Exit(fmt.Errorf("node delete is only available on a Portal node"), 2)
}
key := ctx.Args().First()
if key == "" {
return cli.Exit(fmt.Errorf("node id or name is required"), 2)
}
r, err := reg.NewClientRegistryWithConfig(conf)
if err != nil {
return cli.Exit(err, 1)
}
// Resolve to id for deletion, but also support name.
// Resolve UUID to delete: accept uuid → clientId → name.
var node *reg.Node
if n, findErr := r.FindByNodeUUID(key); findErr == nil && n != nil {
node = n
} else if n, findErr = r.FindByClientID(key); findErr == nil && n != nil {
node = n
} else if name := clean.DNSLabel(key); name != "" {
if n, findErr = r.FindByName(name); findErr == nil && n != nil {
node = n
}
}
if node == nil {
return cli.Exit(fmt.Errorf("node not found"), 3)
}
uuid := node.UUID
dropDB := ctx.Bool("drop-db")
dbName, dbUser := "", ""
if dropDB && node.Database != nil {
dbName = node.Database.Name
dbUser = node.Database.User
}
if ctx.Bool("dry-run") {
log.Infof("dry-run: would delete node %s (uuid=%s, clientId=%s)", clean.LogQuote(node.Name), clean.Log(uuid), clean.Log(node.ClientID))
if ctx.Bool("all-ids") {
log.Infof("dry-run: would remove all registry entries that share uuid %s", clean.Log(uuid))
}
if dropDB {
if dbName == "" && dbUser == "" {
log.Infof("dry-run: --drop-db requested but no database credentials are recorded for node %s", clean.LogQuote(node.Name))
} else {
log.Infof("dry-run: would drop database %s and user %s", clean.Log(dbName), clean.Log(dbUser))
}
}
return nil
}
if !RunNonInteractively(ctx.Bool("yes")) {
prompt := promptui.Prompt{Label: fmt.Sprintf("Delete node %s?", clean.Log(uuid)), IsConfirm: true}
if _, err = prompt.Run(); err != nil {
log.Infof("node %s was not deleted", clean.Log(uuid))
return nil
}
}
if ctx.Bool("all-ids") {
if err = r.DeleteAllByUUID(uuid); err != nil {
return cli.Exit(err, 1)
}
} else if err = r.Delete(uuid); err != nil {
return cli.Exit(err, 1)
}
who := clusterAuditWho(ctx, conf)
event.AuditInfo(append(who,
string(acl.ResourceCluster),
"node", "%s",
status.Deleted,
), clean.Log(uuid))
loggedDeletion := false
if dropDB {
if dbName == "" && dbUser == "" {
log.Infof("node %s has been deleted (no database credentials recorded)", clean.Log(uuid))
loggedDeletion = true
} else {
dropCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := provisioner.DropCredentials(dropCtx, dbName, dbUser); err != nil {
return cli.Exit(fmt.Errorf("failed to drop database credentials for node %s: %w", clean.Log(uuid), err), 1)
}
log.Infof("node %s database %s and user %s have been dropped", clean.Log(uuid), clean.Log(dbName), clean.Log(dbUser))
event.AuditInfo(append(who,
string(acl.ResourceCluster),
"drop database %s user %s",
status.Succeeded,
), clean.Log(dbName), clean.Log(dbUser))
}
}
if !loggedDeletion {
log.Infof("node %s has been deleted", clean.Log(uuid))
}
return nil
})
}