controller: .fab volume legs stream via docker cp (fixes the containerized path-strand data loss, IA finding 1 HIGH) — zero shared paths both deployment shapes; export fails LOUD on any missing/empty claimed tar (no hollow bundles); import validates BEFORE it destroys (hollow bundle → app untouched); docker_run_volume_path_gate.py extinguishes the class (Tier-1/2 mounts documented host-visible)
Claude-Session: https://claude.ai/code/session_01GzammAMzsJTgpQHqxwM2bC
This commit is contained in:
@@ -326,6 +326,16 @@ func (e *Exporter) executeImport(req ImportRequest, job *Job) {
|
||||
job.mu.Unlock()
|
||||
|
||||
e.logger.Printf("[INFO] Import: opening bundle for %s (%s)", manifest.AppName, manifest.DisplayName)
|
||||
|
||||
// v0.125.0 validate-before-destroy (scenario C): every manifest-claimed data tar must be
|
||||
// present and non-empty BEFORE the app is stopped and BEFORE any volume is removed. Hollow
|
||||
// bundles (a containerized v<=0.124.0 exporter stranded the tars host-side) land HERE — the
|
||||
// pre-fix order wiped the volumes first and only then discovered the emptiness.
|
||||
if err := validateBundleData(tmpDir, manifest); err != nil {
|
||||
e.failJob(job, step, fmt.Sprintf("A csomag hiányos — az importálás el sem indult, a meglévő alkalmazás érintetlen. (%v) A csomagot valószínűleg egy régebbi (≤0.124.0), konténerben futó vezérlő exportálta — készíts friss exportot.", err))
|
||||
return
|
||||
}
|
||||
|
||||
e.debugf("step 0 (open bundle) done in %v", time.Since(stepStart))
|
||||
|
||||
job.setStep(step, "done", "")
|
||||
@@ -597,9 +607,9 @@ func (e *Exporter) restoreHDDData(tmpDir string, manifest *Manifest, composePath
|
||||
tarPath := filepath.Join(hddDir, subdir+".tar")
|
||||
tarInfo, err := os.Stat(tarPath)
|
||||
if err != nil {
|
||||
e.logger.Printf("[WARN] Import: HDD tar not found: %s", tarPath)
|
||||
e.debugf("restoreHDDData: tar not found: %s", tarPath)
|
||||
continue
|
||||
// v0.125.0: a claimed-but-absent tar is an ERROR (validateBundleData already refused
|
||||
// it pre-destroy; this is defense in depth, not a reachable soft path).
|
||||
return fmt.Errorf("HDD tar missing from bundle: %s", subdir+".tar")
|
||||
}
|
||||
e.debugf("restoreHDDData: subdir=%s tarSize=%s", subdir, humanizeBytes(tarInfo.Size()))
|
||||
|
||||
@@ -670,7 +680,52 @@ func resolveHDDMounts(composePath string, env map[string]string) []string {
|
||||
return mounts
|
||||
}
|
||||
|
||||
// restoreVolumeData recreates Docker named volumes from bundle tarballs.
|
||||
// validateBundleData asserts every manifest-claimed data tar exists non-empty in the extracted
|
||||
// bundle (v0.125.0, scenario C). Pure read — called before ANY destructive import step.
|
||||
func validateBundleData(tmpDir string, manifest *Manifest) error {
|
||||
for _, v := range manifest.VolumeNames {
|
||||
if err := ValidateSegment("volume_name", v); err != nil {
|
||||
return err
|
||||
}
|
||||
fi, err := os.Stat(filepath.Join(tmpDir, "data", "volumes", v+".tar"))
|
||||
if err != nil || fi.Size() == 0 {
|
||||
return fmt.Errorf("a(z) %q kötet adata hiányzik a csomagból", v)
|
||||
}
|
||||
}
|
||||
for _, s := range manifest.HDDSubdirs {
|
||||
if err := ValidateSegment("hdd_subdir", s); err != nil {
|
||||
return err
|
||||
}
|
||||
fi, err := os.Stat(filepath.Join(tmpDir, "data", "hdd", s+".tar"))
|
||||
if err != nil || fi.Size() == 0 {
|
||||
return fmt.Errorf("a(z) %q adatkönyvtár tartalma hiányzik a csomagból", s)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// importVolumeTar streams tarPath into the (existing) volume via `docker cp - <cid>:/vol` —
|
||||
// zero shared paths, correct on bare metal AND under the containerized controller (the
|
||||
// docker-run -v population was the v0.124.0 strand's import half).
|
||||
func (e *Exporter) importVolumeTar(volName, tarPath string) error {
|
||||
return e.withVolumeHelper(volName, func(cid string) error {
|
||||
f, err := os.Open(tarPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %s: %w", tarPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
if stderr, err := dockerExec(ctx, f, nil, "cp", "-", cid+":/vol"); err != nil {
|
||||
return fmt.Errorf("streaming into volume %s: %s — %w", volName, stderr, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// restoreVolumeData recreates Docker named volumes from bundle tarballs. v0.125.0: a missing
|
||||
// tar is an ERROR (defense in depth behind validateBundleData), and population streams via
|
||||
// docker cp instead of a docker-run -v host mount.
|
||||
func (e *Exporter) restoreVolumeData(tmpDir string, manifest *Manifest) error {
|
||||
volDir := filepath.Join(tmpDir, "data", "volumes")
|
||||
|
||||
@@ -683,34 +738,24 @@ func (e *Exporter) restoreVolumeData(tmpDir string, manifest *Manifest) error {
|
||||
tarPath := filepath.Join(volDir, volName+".tar")
|
||||
tarInfo, err := os.Stat(tarPath)
|
||||
if err != nil {
|
||||
e.logger.Printf("[WARN] Import: volume tar not found: %s", tarPath)
|
||||
e.debugf("restoreVolumeData: tar not found: %s", tarPath)
|
||||
continue
|
||||
return fmt.Errorf("volume tar missing from bundle: %s", volName+".tar")
|
||||
}
|
||||
e.debugf("restoreVolumeData: volume=%s tarSize=%s", volName, humanizeBytes(tarInfo.Size()))
|
||||
|
||||
// Create the Docker volume
|
||||
e.debugf("restoreVolumeData: creating docker volume %s", volName)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
out, err := exec.CommandContext(ctx, "docker", "volume", "create", volName).CombinedOutput()
|
||||
stderr, err := dockerExec(ctx, nil, nil, "volume", "create", volName)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating volume %s: %s — %w", volName, strings.TrimSpace(string(out)), err)
|
||||
return fmt.Errorf("creating volume %s: %s — %w", volName, stderr, err)
|
||||
}
|
||||
e.debugf("restoreVolumeData: volume %s created: %s", volName, strings.TrimSpace(string(out)))
|
||||
|
||||
// Populate volume from tar
|
||||
// Populate volume from tar (docker cp streaming)
|
||||
e.logger.Printf("[INFO] Import: populating volume %s", volName)
|
||||
e.debugf("restoreVolumeData: populating %s via docker run alpine tar xf...", volName)
|
||||
popStart := time.Now()
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
out, err = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"-v", volName+":/vol",
|
||||
"-v", volDir+":/in:ro",
|
||||
"alpine", "tar", "xf", "/in/"+volName+".tar", "-C", "/vol").CombinedOutput()
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("populating volume %s: %s — %w", volName, strings.TrimSpace(string(out)), err)
|
||||
if err := e.importVolumeTar(volName, tarPath); err != nil {
|
||||
return fmt.Errorf("populating volume %s: %w", volName, err)
|
||||
}
|
||||
e.debugf("restoreVolumeData: volume %s populated in %v", volName, time.Since(popStart))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user