refactor: align storage:set with dokku conventions

`storage:set` now takes `<name> <property> [<value>]` like every other `:set` command, where omitting the value unsets the property. Previously it took flags and could not distinguish an empty value from an omitted one, so nothing it set could ever be cleared. The flag form keeps working and emits a deprecation warning.

Annotations and labels move to `storage:annotations:set`, `storage:annotations:report`, `storage:labels:set`, and `storage:labels:report`, matching the `scheduler-k3s` equivalents. These operate on a single key, so clearing one leaves the rest in place rather than replacing the whole map as the `--annotation` and `--label` flags do.
This commit is contained in:
Jose Diaz-Gonzalez
2026-08-10 15:00:45 -04:00
parent 87b240054f
commit 74f9acf7ba
12 changed files with 1624 additions and 500 deletions

View File

@@ -18,17 +18,21 @@ Manage mounted volumes
Additional commands:`
helpContent = `
storage:annotations:report [<name>] [<flag>], Displays annotations for one or more storage entries
storage:annotations:set <name> <key> [<value>], Set or clear an annotation on a storage entry
storage:create <name> [<path>] [flags], Register a named storage entry
storage:destroy <name> [--force] [--destroy-host-dir], Remove a named storage entry (must be unmounted from every app first)
storage:ensure-directory [--chown option] <directory>, [DEPRECATED] use storage:create instead
storage:exec <name> [-- <cmd>...], Run a command (or shell) in a temporary container that mounts the entry
storage:info <name> [--format text|json], Show details for one storage entry
storage:labels:report [<name>] [<flag>], Displays labels for one or more storage entries
storage:labels:set <name> <key> [<value>], Set or clear a label on a storage entry
storage:list <app> [--format text|json], List bind mounts for app's container(s) (host:container)
storage:list-entries [--scheduler s] [--format text|json], List registered storage entries
storage:migrate [<app>|--all], Re-run the legacy -v to attachment migration for an app
storage:mount <app> <host-dir:container-dir>, Create a new bind mount
storage:report [<app>] [<flag>], Displays a storage report for one or more apps
storage:set <name> [flags], Update a storage entry in place
storage:set <name> <property> [<value>], Update a storage entry in place
storage:unmount <app> <host-dir:container-dir>, Remove an existing bind mount
storage:wait <name>, Wait for a storage entry's PVC to be bound (k3s)`
)

View File

@@ -19,6 +19,34 @@ func main() {
switch subcommand {
case "default":
err = storage.CommandHelp()
case "annotations:set":
args := flag.NewFlagSet("storage:annotations:set", flag.ExitOnError)
args.Parse(os.Args[2:])
err = storage.CommandAnnotationsSet(args.Arg(0), args.Arg(1), args.Arg(2))
case "annotations:report":
args := flag.NewFlagSet("storage:annotations:report", flag.ExitOnError)
format := args.String("format", "stdout", "format: [ stdout | json ]")
reportArgs, flagErr := common.ParseReportArgs("storage", os.Args[2:])
if flagErr != nil {
err = flagErr
break
}
args.Parse(reportArgs.OSArgs)
err = storage.CommandAnnotationsReport(args.Arg(0), *format, reportArgs.InfoFlag)
case "labels:set":
args := flag.NewFlagSet("storage:labels:set", flag.ExitOnError)
args.Parse(os.Args[2:])
err = storage.CommandLabelsSet(args.Arg(0), args.Arg(1), args.Arg(2))
case "labels:report":
args := flag.NewFlagSet("storage:labels:report", flag.ExitOnError)
format := args.String("format", "stdout", "format: [ stdout | json ]")
reportArgs, flagErr := common.ParseReportArgs("storage", os.Args[2:])
if flagErr != nil {
err = flagErr
break
}
args.Parse(reportArgs.OSArgs)
err = storage.CommandLabelsReport(args.Arg(0), *format, reportArgs.InfoFlag)
case "create":
args := flag.NewFlagSet("storage:create", flag.ExitOnError)
scheduler := args.String("scheduler", storage.SchedulerDockerLocal, "--scheduler: target scheduler (docker-local, k3s)")
@@ -84,38 +112,73 @@ func main() {
err = storage.CommandList(appName, *format)
case "set":
args := flag.NewFlagSet("storage:set", flag.ExitOnError)
size := args.String("size", "", "--size: new PVC size (k3s)")
accessMode := args.String("access-mode", "", "--access-mode: existing access mode (must match)")
storageClass := args.String("storage-class-name", "", "--storage-class-name: existing storage class (must match)")
namespace := args.String("namespace", "", "--namespace: new namespace")
chown := args.String("chown", "", "--chown: chown option")
mode := args.String("mode", "", "--mode: octal permissions for the host directory, such as 0755 (docker-local only)")
reclaim := args.String("reclaim-policy", "", "--reclaim-policy: reclaim policy for the underlying volume (Retain or Delete)")
annotations := args.StringSlice("annotation", nil, "--annotation key=value: PVC annotation (repeatable, replaces all)")
labels := args.StringSlice("label", nil, "--label key=value: PVC label (repeatable, replaces all)")
args.String("size", "", "--size: [DEPRECATED] use 'storage:set <name> size <value>'")
args.String("access-mode", "", "--access-mode: [DEPRECATED] use 'storage:set <name> access-mode <value>'")
args.String("storage-class-name", "", "--storage-class-name: [DEPRECATED] use 'storage:set <name> storage-class-name <value>'")
args.String("namespace", "", "--namespace: [DEPRECATED] use 'storage:set <name> namespace <value>'")
args.String("chown", "", "--chown: [DEPRECATED] use 'storage:set <name> chown <value>'")
args.String("mode", "", "--mode: [DEPRECATED] use 'storage:set <name> mode <value>'")
args.String("reclaim-policy", "", "--reclaim-policy: [DEPRECATED] use 'storage:set <name> reclaim-policy <value>'")
annotations := args.StringSlice("annotation", nil, "--annotation key=value: [DEPRECATED] use 'storage:annotations:set'")
labels := args.StringSlice("label", nil, "--label key=value: [DEPRECATED] use 'storage:labels:set'")
args.Parse(os.Args[2:])
annotMap, parseErr := parseKVPairs(*annotations)
if parseErr != nil {
err = parseErr
input := storage.CommandSetInput{Name: args.Arg(0)}
property := args.Arg(1)
for _, name := range storage.SettableProperties {
if !args.Changed(name) {
continue
}
value, lookupErr := args.GetString(name)
if lookupErr != nil {
err = lookupErr
break
}
input.Changes = append(input.Changes, storage.PropertyChange{Property: name, Value: value})
}
if err != nil {
break
}
labelMap, parseErr := parseKVPairs(*labels)
if parseErr != nil {
err = parseErr
usedMapFlags := args.Changed("annotation") || args.Changed("label")
if usedMapFlags {
annotMap, parseErr := parseKVPairs(*annotations)
if parseErr != nil {
err = parseErr
break
}
labelMap, parseErr := parseKVPairs(*labels)
if parseErr != nil {
err = parseErr
break
}
input.Annotations = annotMap
input.Labels = labelMap
}
usedFlags := len(input.Changes) > 0 || usedMapFlags
if property != "" && usedFlags {
err = fmt.Errorf("storage:set accepts either a property and a value or flags, not both")
break
}
err = storage.CommandSet(storage.CommandSetInput{
Name: args.Arg(0),
Size: *size,
AccessMode: *accessMode,
StorageClass: *storageClass,
Namespace: *namespace,
Chown: *chown,
Mode: *mode,
ReclaimPolicy: *reclaim,
Annotations: annotMap,
Labels: labelMap,
})
if property == "" && !usedFlags {
err = fmt.Errorf("No property specified")
break
}
if usedFlags {
if len(input.Changes) > 0 {
common.LogWarn("Deprecated: please use 'storage:set <name> <property> [<value>]' instead of flags")
}
if usedMapFlags {
common.LogWarn("Deprecated: please use 'storage:annotations:set' and 'storage:labels:set' instead of --annotation and --label")
}
} else {
input.Changes = append(input.Changes, storage.PropertyChange{Property: property, Value: args.Arg(2)})
}
err = storage.CommandSet(input)
case "exec":
args := flag.NewFlagSet("storage:exec", flag.ExitOnError)
image := args.String("image", "", "--image: container image to use (default alpine:3)")