Container/image/service name, Go module path, CLI binary name, and DOCKER_MIGRATE_* env vars still used the old working name; the project is branded DockMV everywhere else (README, logo, Gitea repo). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
145 lines
4.5 KiB
Go
145 lines
4.5 KiB
Go
package migrate
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/arescom/dockmv/internal/spec"
|
|
)
|
|
|
|
func sample() *spec.Container {
|
|
return &spec.Container{
|
|
ID: "abc123", Name: "app", State: "running", Image: "app:1.0",
|
|
Mounts: []spec.Mount{
|
|
{Kind: spec.MountVolume, Name: "appdata", Destination: "/data", SizeBytes: 4096},
|
|
{Kind: spec.MountBind, Source: "/srv/app/conf", Destination: "/etc/app", ReadOnly: true},
|
|
{Kind: spec.MountAnonymous, Name: strings.Repeat("f", 64), Destination: "/tmp/cache"},
|
|
{Kind: spec.MountTmpfs, Destination: "/run"},
|
|
},
|
|
Endpoints: []spec.Endpoint{{Network: "appnet"}},
|
|
}
|
|
}
|
|
|
|
func TestPrepareDefaultsCopyEverything(t *testing.T) {
|
|
c := sample()
|
|
sel := spec.DefaultSelection(c)
|
|
sel.Include = true
|
|
|
|
p, err := Prepare(c, sel,
|
|
[]spec.Volume{{Name: "appdata", Driver: "local"}},
|
|
[]spec.Network{{Name: "appnet", Driver: "bridge"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// tmpfs carries no data, so exactly the three real locations transfer.
|
|
if len(p.Transfers) != 3 {
|
|
t.Fatalf("expected 3 transfers, got %d: %+v", len(p.Transfers), p.Transfers)
|
|
}
|
|
if len(p.Volumes) != 1 || p.Volumes[0].Name != "appdata" {
|
|
t.Errorf("named volume not scheduled for creation: %+v", p.Volumes)
|
|
}
|
|
if len(p.Networks) != 1 {
|
|
t.Errorf("network not scheduled for creation: %+v", p.Networks)
|
|
}
|
|
|
|
byDest := map[string]Transfer{}
|
|
for _, tr := range p.Transfers {
|
|
byDest[tr.Destination] = tr
|
|
}
|
|
if got := byDest["/data"].RestoreInto; got != "/" {
|
|
t.Errorf("/data must be restored into /, got %q", got)
|
|
}
|
|
if got := byDest["/etc/app"].RestoreInto; got != "/etc" {
|
|
t.Errorf("/etc/app must be restored into /etc, got %q", got)
|
|
}
|
|
if !byDest["/etc/app"].ReadOnly {
|
|
t.Error("read-only bind must be flagged so it is seeded through a staging container")
|
|
}
|
|
}
|
|
|
|
func TestPrepareSkipAndRelocate(t *testing.T) {
|
|
c := sample()
|
|
sel := spec.DefaultSelection(c)
|
|
sel.Include = true
|
|
sel.Mounts["/tmp/cache"] = spec.MountSelection{Action: spec.MountActionSkip}
|
|
sel.Mounts["/etc/app"] = spec.MountSelection{Action: spec.MountActionCopy, TargetSource: "/opt/app/conf"}
|
|
sel.Mounts["/data"] = spec.MountSelection{Action: spec.MountActionStructure, TargetName: "appdata2"}
|
|
|
|
p, err := Prepare(c, sel, []spec.Volume{{Name: "appdata", Driver: "local"}}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !p.Render.DropMounts["/tmp/cache"] {
|
|
t.Error("skipped mount must be dropped from the create command")
|
|
}
|
|
// structure-only means the volume is created but no data is copied.
|
|
for _, tr := range p.Transfers {
|
|
if tr.Destination == "/data" {
|
|
t.Error("a structure-only mount must not be transferred")
|
|
}
|
|
}
|
|
if len(p.Volumes) != 1 || p.Volumes[0].Name != "appdata2" {
|
|
t.Errorf("renamed volume not applied: %+v", p.Volumes)
|
|
}
|
|
|
|
args := strings.Join(p.Target.CreateArgs(p.Render), " ")
|
|
if !strings.Contains(args, "--volume /opt/app/conf:/etc/app:ro") {
|
|
t.Errorf("relocated bind not applied: %s", args)
|
|
}
|
|
if !strings.Contains(args, "--volume appdata2:/data") {
|
|
t.Errorf("renamed volume not applied to create args: %s", args)
|
|
}
|
|
if strings.Contains(args, "/tmp/cache") {
|
|
t.Errorf("skipped mount still present: %s", args)
|
|
}
|
|
}
|
|
|
|
func TestPrepareAnonymousVolumeIsRecreatedFresh(t *testing.T) {
|
|
c := sample()
|
|
sel := spec.DefaultSelection(c)
|
|
sel.Include = true
|
|
|
|
p, err := Prepare(c, sel, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
args := strings.Join(p.Target.CreateArgs(p.Render), " ")
|
|
if strings.Contains(args, strings.Repeat("f", 64)) {
|
|
t.Errorf("the generated volume name must not be pinned on the target: %s", args)
|
|
}
|
|
if !strings.Contains(args, "--volume /tmp/cache") {
|
|
t.Errorf("anonymous volume must still be declared: %s", args)
|
|
}
|
|
}
|
|
|
|
func TestStagingPathsLandArchiveOnTheMountPoint(t *testing.T) {
|
|
// A tar produced from /var/lib/postgresql/data has entries rooted at
|
|
// "data/", so the staging container must mount the volume at
|
|
// <root>/data and extract into <root>.
|
|
mountAt, into := StagingPaths("/var/lib/postgresql/data")
|
|
if mountAt != into+"/data" {
|
|
t.Fatalf("mount point %q is not directly under the extraction dir %q", mountAt, into)
|
|
}
|
|
}
|
|
|
|
func TestPrepareRefusesRootMount(t *testing.T) {
|
|
c := &spec.Container{
|
|
ID: "x", Name: "weird", Image: "img",
|
|
Mounts: []spec.Mount{{Kind: spec.MountBind, Source: "/", Destination: "/"}},
|
|
}
|
|
sel := spec.DefaultSelection(c)
|
|
sel.Include = true
|
|
p, err := Prepare(c, sel, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(p.Transfers) != 0 {
|
|
t.Errorf("a mount at / must not be copied: %+v", p.Transfers)
|
|
}
|
|
if len(p.Notes) == 0 {
|
|
t.Error("refusing to copy / should be reported to the operator")
|
|
}
|
|
}
|