container: check cancel signal delivery
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m50s
Test / Sandbox (race detector) (push) Successful in 3m46s
Test / Planterette (push) Successful in 3m52s
Test / Hakurei (race detector) (push) Successful in 4m28s
Test / Flake checks (push) Successful in 1m18s

This change also makes some parts of the test more robust.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-07-28 01:04:29 +09:00
parent a1e5f020f4
commit 65fe09caf9
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
4 changed files with 90 additions and 30 deletions

View File

@ -71,6 +71,7 @@ type (
Hostname string
// Sequential container setup ops.
*Ops
// Seccomp system call filter rules.
SeccompRules []seccomp.NativeRule
// Extra seccomp flags.
@ -79,6 +80,7 @@ type (
SeccompPresets seccomp.FilterPreset
// Do not load seccomp program.
SeccompDisable bool
// Permission bits of newly created parent directories.
// The zero value is interpreted as 0755.
ParentPerm os.FileMode
@ -121,7 +123,7 @@ func (p *Container) Start() error {
}
p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Args = []string{"init"}
p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil {

View File

@ -12,7 +12,6 @@ import (
"strings"
"syscall"
"testing"
"time"
"hakurei.app/command"
"hakurei.app/container"
@ -21,7 +20,6 @@ import (
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
)
const (
@ -92,18 +90,54 @@ func TestContainer(t *testing.T) {
t.Cleanup(func() { container.SetOutput(oldOutput) })
}
t.Run("cancel", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
ready := make(chan struct{})
if r, w, err := os.Pipe(); err != nil {
t.Fatalf("cannot pipe: %v", err)
} else {
c.ExtraFiles = append(c.ExtraFiles, w)
go func() {
defer close(ready)
if _, err = r.Read(make([]byte, 1)); err != nil {
panic(err.Error())
}
}()
}
if err := c.Start(); err != nil {
hlog.PrintBaseError(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil {
hlog.PrintBaseError(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
<-ready
cancel()
wantErr := context.Canceled
if err := c.Wait(); !errors.Is(err, wantErr) {
hlog.PrintBaseError(err, "wait:")
t.Fatalf("Wait: error = %v, want %v", err, wantErr)
}
})
for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel()
hostname := hostnameFromTestCase(tc.name)
c := container.New(ctx, os.Args[0], "container", strconv.Itoa(i))
prepareHelper(c)
var libPaths []string
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid
c.Gid = tc.gid
c.Hostname = hostname
c.Hostname = hostnameFromTestCase(tc.name)
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
*c.Ops = append(*c.Ops, *tc.ops...)
c.SeccompRules = tc.rules
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
@ -114,38 +148,27 @@ func TestContainer(t *testing.T) {
c.
Tmpfs("/tmp", 0, 0755).
Bind(os.Args[0], os.Args[0], 0).
Place("/etc/hostname", []byte(hostname))
// in case test has cgo enabled
var libPaths []string
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
libPaths = ldd.Path(entries)
}
for _, name := range libPaths {
c.Bind(name, name, 0)
}
Place("/etc/hostname", []byte(c.Hostname))
// needs /proc to check mountinfo
c.Proc("/proc")
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
mnt = append(mnt, ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
mnt = append(mnt, tc.mnt...)
mnt = append(mnt,
// Tmpfs("/tmp", 0, 0755)
ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
// Bind(os.Args[0], os.Args[0], 0)
ent(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
// Place("/etc/hostname", []byte(hostname))
ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Bind(os.Args[0], helperInnerPath, 0)
ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
)
for _, name := range libPaths {
// Bind(name, name, 0)
mnt = append(mnt, ent(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
}
mnt = append(mnt, tc.mnt...)
mnt = append(mnt,
// Tmpfs("/tmp", 0, 0755)
ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
// Place("/etc/hostname", []byte(hostname))
ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Proc("/proc")
ent("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"),
// Place(pathWantMnt, want.Bytes())
@ -206,6 +229,13 @@ func TestContainerString(t *testing.T) {
func init() {
helperCommands = append(helperCommands, func(c command.Command) {
c.Command("block", command.UsageInternal, func(args []string) error {
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
}
select {}
})
c.Command("container", command.UsageInternal, func(args []string) error {
if len(args) != 1 {
return syscall.EINVAL

View File

@ -367,9 +367,11 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
}
}
const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init".
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
msg = v
Init(prepare, setVerbose)
msg.BeforeExit()

View File

@ -1,18 +1,24 @@
package container_test
import (
"context"
"log"
"os"
"testing"
"time"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
)
const (
envDoCheck = "HAKUREI_TEST_DO_CHECK"
helperDefaultTimeout = 5 * time.Second
helperInnerPath = "/usr/bin/helper"
)
var helperCommands []func(c command.Command)
@ -40,4 +46,24 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}
func prepareHelper(c *container.Container) { c.Env = append(c.Env, envDoCheck+"=1") }
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]string, args ...string) (c *container.Container) {
c = container.New(ctx, helperInnerPath, args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(os.Args[0], helperInnerPath, 0)
// in case test has cgo enabled
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
*libPaths = ldd.Path(entries)
}
for _, name := range *libPaths {
c.Bind(name, name, 0)
}
return
}
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]string), args...)
}