mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-11 16:24:11 +01:00
150 lines
4.4 KiB
Go
150 lines
4.4 KiB
Go
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 node’s 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
|
||
})
|
||
}
|