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: "", 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 }) }