mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
CLI: Refactor "dry-run" and "yes" command flags to use helper functions
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -20,10 +20,7 @@ var CleanUpCommand = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cleanUpFlags = []cli.Flag{
|
var cleanUpFlags = []cli.Flag{
|
||||||
&cli.BoolFlag{
|
DryRunFlag("performs a dry run that doesn't actually remove anything"),
|
||||||
Name: "dry",
|
|
||||||
Usage: "performs a dry run that doesn't actually remove anything",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanUpAction removes orphaned index entries, sidecar and thumbnail files.
|
// cleanUpAction removes orphaned index entries, sidecar and thumbnail files.
|
||||||
@@ -49,7 +46,7 @@ func cleanUpAction(ctx *cli.Context) error {
|
|||||||
w := get.CleanUp()
|
w := get.CleanUp()
|
||||||
|
|
||||||
opt := photoprism.CleanUpOptions{
|
opt := photoprism.CleanUpOptions{
|
||||||
Dry: ctx.Bool("dry"),
|
Dry: ctx.Bool("dry-run"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start cleanup worker.
|
// Start cleanup worker.
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ var ClusterNodesModCommand = &cli.Command{
|
|||||||
Name: "mod",
|
Name: "mod",
|
||||||
Usage: "Updates node properties",
|
Usage: "Updates node properties",
|
||||||
ArgsUsage: "<id|name>",
|
ArgsUsage: "<id|name>",
|
||||||
Flags: []cli.Flag{nodesModRoleFlag, nodesModInternal, nodesModLabel, &cli.BoolFlag{Name: "yes", Aliases: []string{"y"}, Usage: "runs the command non-interactively"}},
|
Flags: []cli.Flag{nodesModRoleFlag, nodesModInternal, nodesModLabel,
|
||||||
|
DryRunFlag("preview updates without modifying the registry"),
|
||||||
|
YesFlag(),
|
||||||
|
},
|
||||||
Hidden: true, // Required for cluster-management only.
|
Hidden: true, // Required for cluster-management only.
|
||||||
Action: clusterNodesModAction,
|
Action: clusterNodesModAction,
|
||||||
}
|
}
|
||||||
@@ -62,11 +65,15 @@ func clusterNodesModAction(ctx *cli.Context) error {
|
|||||||
return cli.Exit(fmt.Errorf("node not found"), 3)
|
return cli.Exit(fmt.Errorf("node not found"), 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changes := make([]string, 0, 4)
|
||||||
|
|
||||||
if v := ctx.String("role"); v != "" {
|
if v := ctx.String("role"); v != "" {
|
||||||
n.Role = clean.TypeLowerDash(v)
|
n.Role = clean.TypeLowerDash(v)
|
||||||
|
changes = append(changes, fmt.Sprintf("role=%s", clean.Log(n.Role)))
|
||||||
}
|
}
|
||||||
if v := ctx.String("advertise-url"); v != "" {
|
if v := ctx.String("advertise-url"); v != "" {
|
||||||
n.AdvertiseUrl = v
|
n.AdvertiseUrl = v
|
||||||
|
changes = append(changes, fmt.Sprintf("advertise-url=%s", clean.Log(n.AdvertiseUrl)))
|
||||||
}
|
}
|
||||||
if labels := ctx.StringSlice("label"); len(labels) > 0 {
|
if labels := ctx.StringSlice("label"); len(labels) > 0 {
|
||||||
if n.Labels == nil {
|
if n.Labels == nil {
|
||||||
@@ -77,6 +84,16 @@ func clusterNodesModAction(ctx *cli.Context) error {
|
|||||||
n.Labels[k] = v
|
n.Labels[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
changes = append(changes, fmt.Sprintf("labels+=%s", clean.Log(strings.Join(labels, ","))))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Bool("dry-run") {
|
||||||
|
if len(changes) == 0 {
|
||||||
|
log.Infof("dry-run: no updates to apply for node %s", clean.LogQuote(n.Name))
|
||||||
|
} else {
|
||||||
|
log.Infof("dry-run: would update node %s (%s)", clean.LogQuote(n.Name), strings.Join(changes, ", "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmed := RunNonInteractively(ctx.Bool("yes"))
|
confirmed := RunNonInteractively(ctx.Bool("yes"))
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ var ClusterNodesRemoveCommand = &cli.Command{
|
|||||||
Usage: "Deletes a node from the registry",
|
Usage: "Deletes a node from the registry",
|
||||||
ArgsUsage: "<id|name>",
|
ArgsUsage: "<id|name>",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.BoolFlag{Name: "yes", Aliases: []string{"y"}, Usage: "runs the command non-interactively"},
|
YesFlag(),
|
||||||
&cli.BoolFlag{Name: "all-ids", Usage: "delete all records that share the same UUID (admin cleanup)"},
|
&cli.BoolFlag{Name: "all-ids", Usage: "delete all records that share the same UUID (admin cleanup)"},
|
||||||
&cli.BoolFlag{Name: "drop-db", Aliases: []string{"d"}, Usage: "drop the node’s provisioned database and user after registry deletion"},
|
&cli.BoolFlag{Name: "drop-db", Aliases: []string{"d"}, Usage: "drop the node’s provisioned database and user after registry deletion"},
|
||||||
|
DryRunFlag("preview deletion without modifying the registry or database"),
|
||||||
},
|
},
|
||||||
Hidden: true, // Required for cluster-management only.
|
Hidden: true, // Required for cluster-management only.
|
||||||
Action: clusterNodesRemoveAction,
|
Action: clusterNodesRemoveAction,
|
||||||
@@ -66,33 +67,54 @@ func clusterNodesRemoveAction(ctx *cli.Context) error {
|
|||||||
|
|
||||||
uuid := node.UUID
|
uuid := node.UUID
|
||||||
|
|
||||||
confirmed := RunNonInteractively(ctx.Bool("yes"))
|
|
||||||
if !confirmed {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dropDB := ctx.Bool("drop-db")
|
dropDB := ctx.Bool("drop-db")
|
||||||
dbName, dbUser := "", ""
|
dbName, dbUser := "", ""
|
||||||
|
|
||||||
if dropDB && node.Database != nil {
|
if dropDB && node.Database != nil {
|
||||||
dbName = node.Database.Name
|
dbName = node.Database.Name
|
||||||
dbUser = node.Database.User
|
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") {
|
if ctx.Bool("all-ids") {
|
||||||
if err := r.DeleteAllByUUID(uuid); err != nil {
|
log.Infof("dry-run: would remove all registry entries that share uuid %s", clean.Log(uuid))
|
||||||
return cli.Exit(err, 1)
|
|
||||||
}
|
|
||||||
} else if err := r.Delete(uuid); err != nil {
|
|
||||||
return cli.Exit(err, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
loggedDeletion := false
|
||||||
|
|
||||||
if dropDB {
|
if dropDB {
|
||||||
if dbName == "" && dbUser == "" {
|
if dbName == "" && dbUser == "" {
|
||||||
log.Infof("node %s has been deleted (no database credentials recorded)", clean.Log(uuid))
|
log.Infof("node %s has been deleted (no database credentials recorded)", clean.Log(uuid))
|
||||||
|
loggedDeletion = true
|
||||||
} else {
|
} else {
|
||||||
dropCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
dropCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -103,7 +125,9 @@ func clusterNodesRemoveAction(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !loggedDeletion {
|
||||||
log.Infof("node %s has been deleted", clean.Log(uuid))
|
log.Infof("node %s has been deleted", clean.Log(uuid))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/service/cluster"
|
"github.com/photoprism/photoprism/internal/service/cluster"
|
||||||
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
|
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
"github.com/photoprism/photoprism/pkg/txt/report"
|
"github.com/photoprism/photoprism/pkg/txt/report"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +29,10 @@ var ClusterNodesRotateCommand = &cli.Command{
|
|||||||
Name: "rotate",
|
Name: "rotate",
|
||||||
Usage: "Rotates a node's DB and/or secret via Portal (HTTP)",
|
Usage: "Rotates a node's DB and/or secret via Portal (HTTP)",
|
||||||
ArgsUsage: "<id|name>",
|
ArgsUsage: "<id|name>",
|
||||||
Flags: append([]cli.Flag{rotateDatabaseFlag, rotateSecretFlag, &cli.BoolFlag{Name: "yes", Aliases: []string{"y"}, Usage: "runs the command non-interactively"}, rotatePortalURL, rotatePortalTok}, report.CliFlags...),
|
Flags: append([]cli.Flag{rotateDatabaseFlag, rotateSecretFlag,
|
||||||
|
DryRunFlag("preview rotation without contacting the Portal"),
|
||||||
|
YesFlag(),
|
||||||
|
rotatePortalURL, rotatePortalTok}, report.CliFlags...),
|
||||||
Action: clusterNodesRotateAction,
|
Action: clusterNodesRotateAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,9 +68,6 @@ func clusterNodesRotateAction(ctx *cli.Context) error {
|
|||||||
if portalURL == "" {
|
if portalURL == "" {
|
||||||
portalURL = os.Getenv(config.EnvVar("portal-url"))
|
portalURL = os.Getenv(config.EnvVar("portal-url"))
|
||||||
}
|
}
|
||||||
if portalURL == "" {
|
|
||||||
return cli.Exit(fmt.Errorf("portal URL is required (use --portal-url or set portal-url)"), 2)
|
|
||||||
}
|
|
||||||
token := ctx.String("join-token")
|
token := ctx.String("join-token")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
token = conf.JoinToken()
|
token = conf.JoinToken()
|
||||||
@@ -74,14 +75,41 @@ func clusterNodesRotateAction(ctx *cli.Context) error {
|
|||||||
if token == "" {
|
if token == "" {
|
||||||
token = os.Getenv(config.EnvVar("join-token"))
|
token = os.Getenv(config.EnvVar("join-token"))
|
||||||
}
|
}
|
||||||
if token == "" {
|
|
||||||
return cli.Exit(fmt.Errorf("portal token is required (use --join-token or set join-token)"), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default: rotate DB only if no flag given (safer default)
|
// Default: rotate DB only if no flag given (safer default)
|
||||||
rotateDatabase := ctx.Bool("database") || (!ctx.IsSet("database") && !ctx.IsSet("secret"))
|
rotateDatabase := ctx.Bool("database") || (!ctx.IsSet("database") && !ctx.IsSet("secret"))
|
||||||
rotateSecret := ctx.Bool("secret")
|
rotateSecret := ctx.Bool("secret")
|
||||||
|
|
||||||
|
if ctx.Bool("dry-run") {
|
||||||
|
target := clean.LogQuote(name)
|
||||||
|
if target == "" {
|
||||||
|
target = "(unnamed node)"
|
||||||
|
}
|
||||||
|
var what []string
|
||||||
|
if rotateDatabase {
|
||||||
|
what = append(what, "database credentials")
|
||||||
|
}
|
||||||
|
if rotateSecret {
|
||||||
|
what = append(what, "node secret")
|
||||||
|
}
|
||||||
|
if len(what) == 0 {
|
||||||
|
what = append(what, "no resources (no rotation flags set)")
|
||||||
|
}
|
||||||
|
if portalURL == "" {
|
||||||
|
log.Infof("dry-run: would rotate %s for %s (portal URL not set)", txt.JoinAnd(what), target)
|
||||||
|
} else {
|
||||||
|
log.Infof("dry-run: would rotate %s for %s via %s", txt.JoinAnd(what), target, clean.Log(portalURL))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if portalURL == "" {
|
||||||
|
return cli.Exit(fmt.Errorf("portal URL is required (use --portal-url or set portal-url)"), 2)
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
return cli.Exit(fmt.Errorf("portal token is required (use --join-token or set join-token)"), 2)
|
||||||
|
}
|
||||||
|
|
||||||
confirmed := RunNonInteractively(ctx.Bool("yes"))
|
confirmed := RunNonInteractively(ctx.Bool("yes"))
|
||||||
if !confirmed {
|
if !confirmed {
|
||||||
var what string
|
var what string
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ var (
|
|||||||
regPortalTok = &cli.StringFlag{Name: "join-token", Usage: "Portal access `TOKEN` (defaults to config)"}
|
regPortalTok = &cli.StringFlag{Name: "join-token", Usage: "Portal access `TOKEN` (defaults to config)"}
|
||||||
regWriteConf = &cli.BoolFlag{Name: "write-config", Usage: "persists returned secrets and DB settings to local config"}
|
regWriteConf = &cli.BoolFlag{Name: "write-config", Usage: "persists returned secrets and DB settings to local config"}
|
||||||
regForceFlag = &cli.BoolFlag{Name: "force", Aliases: []string{"f"}, Usage: "confirm actions that may overwrite/replace local data (e.g., --write-config)"}
|
regForceFlag = &cli.BoolFlag{Name: "force", Aliases: []string{"f"}, Usage: "confirm actions that may overwrite/replace local data (e.g., --write-config)"}
|
||||||
regDryRun = &cli.BoolFlag{Name: "dry-run", Usage: "print derived values and payload without performing registration"}
|
regDryRun = DryRunFlag("print derived values and payload without performing registration")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClusterRegisterCommand registers a node with the Portal via HTTP.
|
// ClusterRegisterCommand registers a node with the Portal via HTTP.
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ func JsonFlag() *cli.BoolFlag {
|
|||||||
return &cli.BoolFlag{Name: "json", Aliases: []string{"j"}, Usage: "print machine-readable JSON"}
|
return &cli.BoolFlag{Name: "json", Aliases: []string{"j"}, Usage: "print machine-readable JSON"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DryRunFlag returns the shared CLI flag definition for dry runs across commands.
|
||||||
|
func DryRunFlag(usage string) *cli.BoolFlag {
|
||||||
|
if usage == "" {
|
||||||
|
usage = "performs a dry run without making any destructive changes"
|
||||||
|
}
|
||||||
|
return &cli.BoolFlag{Name: "dry-run", Aliases: []string{"dry"}, Usage: usage}
|
||||||
|
}
|
||||||
|
|
||||||
|
// YesFlag returns the shared CLI flag definition for non-interactive runs across commands.
|
||||||
|
func YesFlag() *cli.BoolFlag {
|
||||||
|
return &cli.BoolFlag{Name: "yes", Aliases: []string{"y"}, Usage: "runs the command non-interactively"}
|
||||||
|
}
|
||||||
|
|
||||||
// PicturesCountFlag returns a shared flag definition limiting how many pictures a batch operation processes.
|
// PicturesCountFlag returns a shared flag definition limiting how many pictures a batch operation processes.
|
||||||
// Usage: commands from the vision or import tooling that need to cap result size per invocation.
|
// Usage: commands from the vision or import tooling that need to cap result size per invocation.
|
||||||
func PicturesCountFlag() *cli.IntFlag {
|
func PicturesCountFlag() *cli.IntFlag {
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ var purgeFlags = []cli.Flag{
|
|||||||
Name: "hard",
|
Name: "hard",
|
||||||
Usage: "permanently removes data from the index",
|
Usage: "permanently removes data from the index",
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
DryRunFlag("performs a dry run that doesn't actually remove anything"),
|
||||||
Name: "dry",
|
|
||||||
Usage: "performs a dry run that doesn't actually remove anything",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// purgeAction removes missing files from search results
|
// purgeAction removes missing files from search results
|
||||||
@@ -67,7 +64,7 @@ func purgeAction(ctx *cli.Context) error {
|
|||||||
|
|
||||||
opt := photoprism.PurgeOptions{
|
opt := photoprism.PurgeOptions{
|
||||||
Path: subPath,
|
Path: subPath,
|
||||||
Dry: ctx.Bool("dry"),
|
Dry: ctx.Bool("dry-run"),
|
||||||
Hard: ctx.Bool("hard"),
|
Hard: ctx.Bool("hard"),
|
||||||
Force: true,
|
Force: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ var VisionRunCommand = &cli.Command{
|
|||||||
Aliases: []string{"f"},
|
Aliases: []string{"f"},
|
||||||
Usage: "replaces existing data if the model supports it and the source priority is equal or higher",
|
Usage: "replaces existing data if the model supports it and the source priority is equal or higher",
|
||||||
},
|
},
|
||||||
|
DryRunFlag("preview the run without executing any models"),
|
||||||
},
|
},
|
||||||
Action: visionRunAction,
|
Action: visionRunAction,
|
||||||
}
|
}
|
||||||
@@ -45,10 +46,21 @@ func visionRunAction(ctx *cli.Context) error {
|
|||||||
return cli.Exit(err.Error(), 1)
|
return cli.Exit(err.Error(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
models := vision.ParseModelTypes(ctx.String("models"))
|
||||||
|
|
||||||
|
if ctx.Bool("dry-run") {
|
||||||
|
modelList := strings.Join(models, ",")
|
||||||
|
if modelList == "" {
|
||||||
|
modelList = "(none)"
|
||||||
|
}
|
||||||
|
log.Infof("dry-run: vision run would execute models [%s] with filter=%q (count=%d, source=%s, force=%v)", modelList, filter, ctx.Int("count"), string(source), ctx.Bool("force"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return worker.Start(
|
return worker.Start(
|
||||||
filter,
|
filter,
|
||||||
ctx.Int("count"),
|
ctx.Int("count"),
|
||||||
vision.ParseModelTypes(ctx.String("models")),
|
models,
|
||||||
string(source),
|
string(source),
|
||||||
ctx.Bool("force"),
|
ctx.Bool("force"),
|
||||||
vision.RunManual,
|
vision.RunManual,
|
||||||
|
|||||||
Reference in New Issue
Block a user