controller v0.268.0: the undo finds volumes by definition; a held app names only a whole copy; one press = one tested step (R-658, R-659, R-660, R-651; 09 §6.4 part 5)
gates / gates (push) Successful in 27s
gates / gates (push) Successful in 27s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -139,19 +139,32 @@ func (m *Manager) restoreDockerVolumesFrom(stackName, dumpDir string) (int, erro
|
||||
|
||||
var restored int
|
||||
var failed []string
|
||||
composeVer := ""
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".tar") {
|
||||
continue
|
||||
}
|
||||
volName := strings.TrimSuffix(entry.Name(), ".tar")
|
||||
if composeVer == "" {
|
||||
composeVer = composeVersionShort()
|
||||
}
|
||||
|
||||
m.logger.Printf("[INFO] [backup] Restoring Docker volume %s for %s", volName, stackName)
|
||||
|
||||
// Remove existing volume (ignore errors — may not exist)
|
||||
dockerexec.Command("docker", "volume", "rm", "-f", volName).Run()
|
||||
|
||||
// Create fresh volume
|
||||
if out, err := dockerexec.Command("docker", "volume", "create", volName).CombinedOutput(); err != nil {
|
||||
// Create fresh volume — WITH the labels compose gives its own volumes (R-658, v0.268.0). A bare
|
||||
// `volume create` made a volume the update's undo (until v0.267.0) could not find, and one
|
||||
// compose warns it "was not created by Docker Compose". The config-hash label is deliberately
|
||||
// NOT set: compose only compares it when present, and a guessed hash would make it offer to
|
||||
// recreate (empty) the volume.
|
||||
args := append([]string{"volume", "create"}, composeVolumeLabelArgs(stackName, volName, composeVer)...)
|
||||
if len(args) == 2 {
|
||||
m.logger.Printf("[WARN] [backup] volume %s does not carry the %s_ prefix — created WITHOUT compose labels (the undo finds it by the app's definition anyway)", volName, stackName)
|
||||
}
|
||||
args = append(args, volName)
|
||||
if out, err := dockerexec.Command("docker", args...).CombinedOutput(); err != nil {
|
||||
m.logger.Printf("[ERROR] [backup] Failed to create volume %s: %s — %v", volName, strings.TrimSpace(string(out)), err)
|
||||
failed = append(failed, volName)
|
||||
continue
|
||||
@@ -215,3 +228,29 @@ func (m *Manager) waitForHealthy(stackName string, timeout time.Duration) error
|
||||
}
|
||||
return fmt.Errorf("stack %s did not reach running state within %s after restore", stackName, timeout)
|
||||
}
|
||||
|
||||
// composeVolumeLabelArgs returns the `--label` arguments that make a restored volume look like one
|
||||
// compose created for this project: project, volume key and compose version. The key is the part of
|
||||
// the Docker name after `<project>_`, which is how compose names a volume without its own `name:` —
|
||||
// every catalog template today (R-658, measured 2026-09-24). A name without that prefix gets no labels
|
||||
// rather than a guessed key. The version label is left out when the version could not be read.
|
||||
func composeVolumeLabelArgs(project, volName, composeVersion string) []string {
|
||||
key := strings.TrimPrefix(volName, project+"_")
|
||||
if key == volName || key == "" {
|
||||
return nil
|
||||
}
|
||||
args := []string{"--label", "com.docker.compose.project=" + project, "--label", "com.docker.compose.volume=" + key}
|
||||
if composeVersion != "" {
|
||||
args = append(args, "--label", "com.docker.compose.version="+composeVersion)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// composeVersionShort is `docker compose version --short`, "" when it cannot be read.
|
||||
func composeVersionShort() string {
|
||||
out, err := dockerexec.Command("docker", "compose", "version", "--short").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimPrefix(strings.TrimSpace(string(out)), "v")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user