Files
felhom-controller/controller/internal/appexport/r1279_pull_first_test.go
T

95 lines
4.1 KiB
Go

package appexport
import (
"errors"
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
)
// R-1279 (brief 2026-10-26 fix B) — the `.fab` import downloads first. Before (`e8d2151`): step 1 removed the RUNNING
// app's volumes, and the first pull came later at the database start, so a version that could not be downloaded left
// the app gone. Now the D48 helper runs before step 1 and a refusal changes nothing.
// COMPANION RED-PROOF (REPORT.md): drop the prepareImages call in restore.go → TestR1279_* fails (the volumes are
// removed and the app is stopped before anything is pulled).
func r1279Bundle(t *testing.T) (fab, stackDir, stacksDir string) {
t.Helper()
tree := t.TempDir()
os.MkdirAll(filepath.Join(tree, "config"), 0755)
os.WriteFile(filepath.Join(tree, "config", "docker-compose.yml"), []byte("services:\n kuma:\n image: louislam/uptime-kuma:0.0.0-missing\n"), 0644)
man := &Manifest{Version: ManifestVersion, AppName: "kuma", DisplayName: "Kuma", ConfigFiles: []string{"docker-compose.yml"}}
data, err := man.Marshal()
if err != nil {
t.Fatal(err)
}
os.WriteFile(filepath.Join(tree, "manifest.json"), data, 0644)
fab = filepath.Join(t.TempDir(), "kuma.fab")
if err := createTarGz(fab, tree); err != nil {
t.Fatal(err)
}
stacksDir = t.TempDir()
stackDir = filepath.Join(stacksDir, "kuma")
os.MkdirAll(stackDir, 0755)
os.WriteFile(filepath.Join(stackDir, "docker-compose.yml"), []byte("LIVE"), 0644)
return fab, stackDir, stacksDir
}
func TestR1279_AnUnpullableImageRefusesTheImportBeforeAnythingIsRemoved(t *testing.T) {
calls := swapDockerExec(t, func(c dockerCall, stdin io.Reader, stdout io.Writer) (string, error) { return "", nil })
fab, stackDir, stacksDir := r1279Bundle(t)
prov := &rtProvider{stackDir: stackDir, stacksDir: stacksDir, deployed: true, running: true, volumes: []string{"kuma_data"}}
e := NewExporter(prov, log.New(io.Discard, "", 0), "test")
var asked string
gone := util.MsgErrorf(errors.New("gone"), "err.backup.restore_image_gone", "uptime-kuma:0.0.0-missing")
e.SetImagePreparer(func(app, compose string) (func(), error) {
b, _ := os.ReadFile(compose)
asked = string(b)
return nil, gone
})
if err := e.StartImport(ImportRequest{FABPath: fab}); err != nil {
t.Fatalf("StartImport: %v", err)
}
msg := jobErr(waitJob(t, e))
if !strings.Contains(asked, "0.0.0-missing") {
t.Fatalf("the helper must be asked about the BUNDLE's definition (the one that will run), got %q", asked)
}
if !strings.Contains(msg, "nem lehet letölteni") || !strings.Contains(msg, "nem nyúltunk") {
t.Fatalf("the household reads the D48 sentence, got %q", msg)
}
if prov.stopped != 0 || prov.removed != 0 || prov.started || len(*calls) != 0 {
t.Fatalf("a refused import must change nothing: stopped=%d removed=%d started=%t docker=%v", prov.stopped, prov.removed, prov.started, *calls)
}
if b, _ := os.ReadFile(filepath.Join(stackDir, "docker-compose.yml")); string(b) != "LIVE" {
t.Fatalf("the app's files were changed: %q", b)
}
}
// A pullable image: asked first, then the import goes on as before (the running app's volumes removed after the pull).
func TestR1279_APullableImageIsAskedFirstAndTheImportGoesOn(t *testing.T) {
swapDockerExec(t, func(c dockerCall, stdin io.Reader, stdout io.Writer) (string, error) { return "", nil })
fab, stackDir, stacksDir := r1279Bundle(t)
prov := &rtProvider{stackDir: stackDir, stacksDir: stacksDir, deployed: true, running: true, volumes: []string{"kuma_data"}}
e := NewExporter(prov, log.New(io.Discard, "", 0), "test")
removedAtAsk, released := -1, false
e.SetImagePreparer(func(string, string) (func(), error) {
removedAtAsk = prov.removed
return func() { released = true }, nil
})
if err := e.StartImport(ImportRequest{FABPath: fab}); err != nil {
t.Fatalf("StartImport: %v", err)
}
waitJob(t, e)
if removedAtAsk != 0 || prov.removed != 1 {
t.Fatalf("the pull must come BEFORE the running app's volumes are removed: removed at ask %d, after %d", removedAtAsk, prov.removed)
}
if !released {
t.Fatal("the image-work hold must be released when the import ends")
}
}