mesh/delivery merged: source.main moved to novox/mesh-host@3e3e43de5214
mesh/check pass 175fb45c19e3 on 3e3e43de5214 | unit 299 run, 1042 kept, 5 skipped | integration 302 run, 1043 kept, 4 skipped | over bound: unit (slow…
220 lines
7.8 KiB
Go
220 lines
7.8 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// Issue 1742: the engine removed a running container and only then found the replacement image
|
|
// unobtainable, leaving the service down. The replacement image comes first.
|
|
func TestARunningContainerIsKeptWhenItsReplacementImageCannotBeObtained(t *testing.T) {
|
|
d := parseTrusted(t, `{"declaration":1,"resources":[
|
|
{"id":"store","type":"container","name":"store","image":"`+pinned+`","env":{"PGDATA":"/data"}}
|
|
]}`)
|
|
var removed, created bool
|
|
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
|
args = oldShape(name, args)
|
|
switch args[0] {
|
|
case "info":
|
|
return "27.0\n", nil
|
|
case "container":
|
|
return "true\tsome-older-spec", nil
|
|
case "image":
|
|
return "", errors.New("no such image")
|
|
case "pull":
|
|
return "", errors.New("manifest unknown")
|
|
case "rm":
|
|
removed = true
|
|
return "", nil
|
|
case "run":
|
|
created = true
|
|
return "deadbeef\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
|
if removed || created {
|
|
t.Fatalf("the running container was touched although its image is unobtainable (removed=%v created=%v)", removed, created)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("an unobtainable image failed nothing")
|
|
}
|
|
if !strings.Contains(err.Error(), pinned) {
|
|
t.Errorf("the failure does not name the image %s: %v", pinned, err)
|
|
}
|
|
}
|
|
|
|
func TestAReplacementImageIsPresentBeforeTheRunningContainerIsRemoved(t *testing.T) {
|
|
d := parseTrusted(t, `{"declaration":1,"resources":[
|
|
{"id":"store","type":"container","name":"store","image":"`+pinned+`","env":{"PGDATA":"/data"}}
|
|
]}`)
|
|
want := containerSpec(d.Resources[0].(*declaration.Container), inputs{})
|
|
var order []string
|
|
created := false
|
|
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
|
args = oldShape(name, args)
|
|
switch args[0] {
|
|
case "info":
|
|
return "27.0\n", nil
|
|
case "container":
|
|
if created {
|
|
return "true\t" + want, nil
|
|
}
|
|
return "true\tsome-older-spec", nil
|
|
case "image":
|
|
order = append(order, "inspect")
|
|
if len(order) == 1 {
|
|
return "", errors.New("no such image")
|
|
}
|
|
return "", nil
|
|
case "pull":
|
|
order = append(order, "pull")
|
|
case "rm":
|
|
order = append(order, "rm")
|
|
case "run":
|
|
order = append(order, "run")
|
|
created = true
|
|
return "deadbeef\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil); err != nil {
|
|
t.Fatalf("apply failed: %v", err)
|
|
}
|
|
got := strings.Join(order, ",")
|
|
if !strings.HasPrefix(got, "inspect,pull,inspect,rm,run") {
|
|
t.Errorf("order of runtime calls = %s, want the image pulled before rm", got)
|
|
}
|
|
}
|
|
|
|
// asideRun is a runtime whose container "store" runs from an older spec, recording the steps a replacement takes; run
|
|
// fails when failRun, and the new container reads back as declared otherwise.
|
|
func asideRun(t *testing.T, want string, failRun bool, order *[]string) Runner {
|
|
created := false
|
|
return func(ctx context.Context, name string, args ...string) (string, error) {
|
|
switch args[0] {
|
|
case "info":
|
|
return "27.0\n", nil
|
|
case "container":
|
|
if created {
|
|
return "true\t" + want, nil
|
|
}
|
|
return "true\tsome-older-spec", nil
|
|
case "image":
|
|
return "", nil
|
|
case "rm", "rename", "stop", "start":
|
|
*order = append(*order, strings.Join(args, " "))
|
|
return "", nil
|
|
case "run":
|
|
*order = append(*order, "run")
|
|
if failRun {
|
|
return "", errors.New("port is already allocated")
|
|
}
|
|
created = true
|
|
return "deadbeef\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
}
|
|
|
|
// A container whose replacement does not come up is put back: the running one is set aside (renamed and stopped), never
|
|
// removed before the new one runs, and started again under its name when the new one fails, the apply failing loudly.
|
|
// Removed first, the bus's own container left novox without a bus when its replacement was never made (issue 1945,
|
|
// live on 2026-10-25 at 11:23).
|
|
func TestAContainerWhoseReplacementFailsIsPutBack(t *testing.T) {
|
|
d := parseTrusted(t, `{"declaration":1,"resources":[
|
|
{"id":"store","type":"container","name":"store","image":"`+pinned+`","env":{"PGDATA":"/data"}}
|
|
]}`)
|
|
want := containerSpec(d.Resources[0].(*declaration.Container), inputs{})
|
|
var order []string
|
|
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, asideRun(t, want, true, &order), nil, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "started again") {
|
|
t.Fatalf("a failed replacement: %v", err)
|
|
}
|
|
got := strings.Join(order, "; ")
|
|
if !strings.Contains(got, "rename store store.replaced; stop store.replaced; run") ||
|
|
!strings.Contains(got, "rename store.replaced store; start store") {
|
|
t.Fatalf("the steps were %q", got)
|
|
}
|
|
if strings.Contains(got, "rm -f store;") && strings.Index(got, "rm -f store;") < strings.Index(got, "run") {
|
|
t.Fatalf("the running container was removed before its replacement ran: %q", got)
|
|
}
|
|
}
|
|
|
|
// A replacement that runs removes the container set aside, and only after it runs.
|
|
func TestAReplacementThatRunsRemovesTheContainerSetAside(t *testing.T) {
|
|
d := parseTrusted(t, `{"declaration":1,"resources":[
|
|
{"id":"store","type":"container","name":"store","image":"`+pinned+`","env":{"PGDATA":"/data"}}
|
|
]}`)
|
|
want := containerSpec(d.Resources[0].(*declaration.Container), inputs{})
|
|
var order []string
|
|
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, asideRun(t, want, false, &order), nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := strings.Join(order, "; ")
|
|
run, removed := strings.Index(got, "run"), strings.LastIndex(got, "rm -f store.replaced")
|
|
if run < 0 || removed < run || strings.Contains(got, "start store") {
|
|
t.Fatalf("the steps were %q: the set-aside container is removed after the replacement runs", got)
|
|
}
|
|
}
|
|
|
|
// A container left set aside by an engine that stopped between setting it aside and running its replacement is the
|
|
// set-aside one of the next apply: put back when the replacement fails, removed once it runs (the review of issue
|
|
// 1945).
|
|
func TestAContainerLeftAsideIsPutBackOrRemovedByTheNextApply(t *testing.T) {
|
|
d := parseTrusted(t, `{"declaration":1,"resources":[
|
|
{"id":"store","type":"container","name":"store","image":"`+pinned+`","env":{"PGDATA":"/data"}}
|
|
]}`)
|
|
want := containerSpec(d.Resources[0].(*declaration.Container), inputs{})
|
|
for _, failRun := range []bool{true, false} {
|
|
var order []string
|
|
created := false
|
|
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
|
switch args[0] {
|
|
case "info":
|
|
return "27.0\n", nil
|
|
case "container":
|
|
switch args[len(args)-1] {
|
|
case "store":
|
|
if created {
|
|
return "true\t" + want, nil
|
|
}
|
|
return "", errors.New("no such container")
|
|
case "store.replaced":
|
|
return "false\tsome-older-spec", nil
|
|
}
|
|
return "", errors.New("no such container")
|
|
case "image":
|
|
return "", nil
|
|
case "rm", "rename", "stop", "start":
|
|
order = append(order, strings.Join(args, " "))
|
|
return "", nil
|
|
case "run":
|
|
order = append(order, "run")
|
|
if failRun {
|
|
return "", errors.New("port is already allocated")
|
|
}
|
|
created = true
|
|
return "deadbeef\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
|
got := strings.Join(order, "; ")
|
|
if failRun {
|
|
if err == nil || !strings.Contains(got, "rename store.replaced store; start store") {
|
|
t.Fatalf("a failed run beside a container left aside: %v; steps %q", err, got)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil || !strings.Contains(got, "run; rm -f store.replaced") {
|
|
t.Fatalf("a run beside a container left aside: %v; steps %q", err, got)
|
|
}
|
|
}
|
|
}
|