feat: add storage:create, storage:destroy, storage:info, storage:list-entries

Wires the new entry-management commands on top of the Entry type. storage:create accepts both docker-local entries (with idempotent host-directory creation) and k3s entries (which delegate PVC provisioning to a forthcoming scheduler-side storage-create trigger). storage:destroy refuses to remove an entry that any app still mounts. storage:ensure-directory now emits a deprecation warning. storage:mount and storage:unmount keep the existing colon-form behavior so this commit is non-breaking; the named-entry mount form follows in a later commit.
This commit is contained in:
Jose Diaz-Gonzalez
2026-04-29 14:39:59 -04:00
parent 4e87d1f447
commit 8a46d89181
5 changed files with 381 additions and 3 deletions

View File

@@ -18,8 +18,12 @@ Manage mounted volumes
Additional commands:`
helpContent = `
storage:ensure-directory [--chown option] <directory>, Creates a persistent storage directory in the recommended storage path
storage:create <name> [<path>] [flags], Register a named storage entry
storage:destroy <name>, Remove a named storage entry (must be unmounted from every app first)
storage:ensure-directory [--chown option] <directory>, [DEPRECATED] use storage:create instead
storage:info <name> [--format text|json], Show details for one 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: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:unmount <app> <host-dir:container-dir>, Remove an existing bind mount`

View File

@@ -19,18 +19,71 @@ func main() {
switch subcommand {
case "default":
err = storage.CommandHelp()
case "create":
args := flag.NewFlagSet("storage:create", flag.ExitOnError)
scheduler := args.String("scheduler", storage.SchedulerDockerLocal, "--scheduler: target scheduler (docker-local, k3s)")
size := args.String("size", "", "--size: PVC size (k3s only, e.g. 2Gi)")
accessMode := args.String("access-mode", "", "--access-mode: PVC access mode (k3s only)")
storageClass := args.String("storage-class-name", "", "--storage-class-name: PVC storage class (k3s only)")
namespace := args.String("namespace", "", "--namespace: PVC namespace (k3s only)")
chown := args.String("chown", "", "--chown: chown option (docker-local only)")
reclaim := args.String("reclaim-policy", "", "--reclaim-policy: PV reclaim policy (Retain or Delete, k3s only)")
annotations := args.StringSlice("annotation", nil, "--annotation key=value: PVC annotation (repeatable)")
labels := args.StringSlice("label", nil, "--label key=value: PVC label (repeatable)")
args.Parse(os.Args[2:])
name := args.Arg(0)
path := args.Arg(1)
annotMap, parseErr := parseKVPairs(*annotations)
if parseErr != nil {
err = parseErr
break
}
labelMap, parseErr := parseKVPairs(*labels)
if parseErr != nil {
err = parseErr
break
}
err = storage.CommandCreate(storage.CommandCreateInput{
Name: name,
Path: path,
Scheduler: *scheduler,
Size: *size,
AccessMode: *accessMode,
StorageClass: *storageClass,
Namespace: *namespace,
Chown: *chown,
ReclaimPolicy: *reclaim,
Annotations: annotMap,
Labels: labelMap,
})
case "destroy":
args := flag.NewFlagSet("storage:destroy", flag.ExitOnError)
args.Parse(os.Args[2:])
err = storage.CommandDestroy(args.Arg(0))
case "ensure-directory":
args := flag.NewFlagSet("storage:ensure-directory", flag.ExitOnError)
chown := args.String("chown", "herokuish", "--chown: chown option (herokuish, heroku, paketo, root, false)")
args.Parse(os.Args[2:])
directory := args.Arg(0)
common.LogWarn("storage:ensure-directory is deprecated; use storage:create instead.")
err = storage.CommandEnsureDirectory(directory, *chown)
case "info":
args := flag.NewFlagSet("storage:info", flag.ExitOnError)
format := args.String("format", "text", "--format: output format (text, json)")
args.Parse(os.Args[2:])
err = storage.CommandInfo(args.Arg(0), *format)
case "list":
args := flag.NewFlagSet("storage:list", flag.ExitOnError)
format := args.String("format", "text", "--format: output format (text, json)")
args.Parse(os.Args[2:])
appName := args.Arg(0)
err = storage.CommandList(appName, *format)
case "list-entries":
args := flag.NewFlagSet("storage:list-entries", flag.ExitOnError)
scheduler := args.String("scheduler", "", "--scheduler: filter to a single scheduler")
format := args.String("format", "text", "--format: output format (text, json)")
args.Parse(os.Args[2:])
err = storage.CommandListEntries(*scheduler, *format)
case "mount":
args := flag.NewFlagSet("storage:mount", flag.ExitOnError)
args.Parse(os.Args[2:])
@@ -63,3 +116,18 @@ func main() {
common.LogFailWithError(err)
}
}
func parseKVPairs(pairs []string) (map[string]string, error) {
if len(pairs) == 0 {
return nil, nil
}
out := map[string]string{}
for _, pair := range pairs {
idx := strings.Index(pair, "=")
if idx <= 0 {
return nil, fmt.Errorf("expected key=value, got %q", pair)
}
out[pair[:idx]] = pair[idx+1:]
}
return out, nil
}