feat: storage attachments, docker-args triggers, install-time migration
storage:mount and storage:unmount now accept the named-entry form (--container-dir, --phase, --process-type, --volume-subpath, --volume-readonly, --volume-chown), persisting attachments via the property list. The legacy host:container colon form is preserved for back-compat. Storage now emits its own docker-args-deploy and docker-args-run triggers so docker-local apps continue to receive bind mounts even after their data moves out of docker-options. The install trigger walks every app once per upgrade and converts legacy -v lines into legacy-<hash> entries plus attachments, draining the originals from docker-options last so a partial run leaves the old behavior intact. Lifecycle triggers post-delete, post-app-clone-setup, and post-app-rename-setup keep the attachment store in sync with app state.
This commit is contained in:
@@ -6,11 +6,13 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
// TriggerInstall sets up the storage plugin on installation
|
||||
// TriggerInstall sets up the storage plugin on installation and runs
|
||||
// the bulk legacy-mount migration once per upgrade.
|
||||
func TriggerInstall() error {
|
||||
storageDir := GetStorageDirectory()
|
||||
|
||||
@@ -25,6 +27,14 @@ func TriggerInstall() error {
|
||||
return fmt.Errorf("Unable to set storage directory permissions: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := EnsureEntriesDirectory(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := MigrateLegacyMounts(); err != nil {
|
||||
return fmt.Errorf("storage migration failed: %w", err)
|
||||
}
|
||||
|
||||
distro := detectDistro()
|
||||
if distro == "" {
|
||||
return nil
|
||||
@@ -44,6 +54,44 @@ func TriggerInstall() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerPostDelete clears any attachment list for an app that's being
|
||||
// destroyed. Entries are global and survive app deletion.
|
||||
func TriggerPostDelete(appName string) error {
|
||||
if appName == "" {
|
||||
return nil
|
||||
}
|
||||
return common.PropertyListWrite(PluginName, appName, AttachmentsProperty, []string{})
|
||||
}
|
||||
|
||||
// TriggerPostAppCloneSetup copies attachments from the source app to the
|
||||
// cloned app. Entries are global so no entry-level work is needed.
|
||||
func TriggerPostAppCloneSetup(oldName string, newName string) error {
|
||||
if oldName == "" || newName == "" {
|
||||
return nil
|
||||
}
|
||||
attachments, err := LoadAttachments(oldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return SaveAttachments(newName, attachments)
|
||||
}
|
||||
|
||||
// TriggerPostAppRenameSetup moves attachments from the old name to the
|
||||
// new name and clears the old.
|
||||
func TriggerPostAppRenameSetup(oldName string, newName string) error {
|
||||
if oldName == "" || newName == "" {
|
||||
return nil
|
||||
}
|
||||
attachments, err := LoadAttachments(oldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := SaveAttachments(newName, attachments); err != nil {
|
||||
return err
|
||||
}
|
||||
return common.PropertyListWrite(PluginName, oldName, AttachmentsProperty, []string{})
|
||||
}
|
||||
|
||||
// detectDistro returns the Linux distribution name
|
||||
func detectDistro() string {
|
||||
if runtime.GOOS != "linux" {
|
||||
@@ -91,3 +139,87 @@ func TriggerStorageList(appName string, phase string, format string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppMountPair pairs an Attachment with the Entry it references; consumed
|
||||
// by scheduler plugins via the storage-app-mounts trigger.
|
||||
type AppMountPair struct {
|
||||
Entry *Entry `json:"entry"`
|
||||
Attachment *Attachment `json:"attachment"`
|
||||
}
|
||||
|
||||
// TriggerStorageAppMounts emits the (entry, attachment) pairs an app has
|
||||
// for the given phase, in JSON. Schedulers consume this at deploy time.
|
||||
func TriggerStorageAppMounts(appName string, phase string) error {
|
||||
if phase == "" {
|
||||
phase = PhaseDeploy
|
||||
}
|
||||
attachments, err := AttachmentsForPhase(appName, phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pairs := []AppMountPair{}
|
||||
for _, attachment := range attachments {
|
||||
entry, err := LoadEntry(attachment.EntryName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("attachment on %q references missing entry %q: %w", appName, attachment.EntryName, err)
|
||||
}
|
||||
pairs = append(pairs, AppMountPair{Entry: entry, Attachment: attachment})
|
||||
}
|
||||
|
||||
output, err := json.Marshal(pairs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerDockerArgs emits `-v` flags for each docker-local attachment in
|
||||
// the requested phase. Plugn concatenates this with docker-options'
|
||||
// equivalent trigger output, so docker-local apps continue to receive
|
||||
// their bind mounts through the standard pipeline.
|
||||
func TriggerDockerArgs(appName string, phase string) error {
|
||||
attachments, err := AttachmentsForPhase(appName, phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, attachment := range attachments {
|
||||
entry, err := LoadEntry(attachment.EntryName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("attachment on %q references missing entry %q: %w", appName, attachment.EntryName, err)
|
||||
}
|
||||
if entry.Scheduler != SchedulerDockerLocal {
|
||||
continue
|
||||
}
|
||||
flag := buildDockerVFlag(entry, attachment)
|
||||
if flag == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" %s", flag)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildDockerVFlag formats the Docker -v argument for a docker-local
|
||||
// attachment.
|
||||
func buildDockerVFlag(entry *Entry, attachment *Attachment) string {
|
||||
host := entry.HostPath
|
||||
container := attachment.ContainerPath
|
||||
if host == "" || container == "" {
|
||||
return ""
|
||||
}
|
||||
options := []string{}
|
||||
if attachment.Readonly {
|
||||
options = append(options, "ro")
|
||||
}
|
||||
if attachment.VolumeOptions != "" {
|
||||
options = append(options, attachment.VolumeOptions)
|
||||
}
|
||||
flag := fmt.Sprintf("-v %s:%s", host, container)
|
||||
if len(options) > 0 {
|
||||
flag = fmt.Sprintf("%s:%s", flag, strings.Join(options, ","))
|
||||
}
|
||||
return flag
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user