Files
podman/pkg/systemd/notifyproxy/notifyproxy_test.go
T
Tobias Wiesing 2132e0918b pkg/systemd/notifyproxy: wait for a barrier before exiting
systemd attributes a notification to a unit by the cgroup of its sender
when it reads the message. The podman process that sends READY for
--sdnotify=healthy exits right after sending it, and when systemd reads
the message only then, it is dropped and the unit runs into its start
timeout.

Add SendMessageWithBarrier, which sends BARRIER=1 with a pipe fd after
the message and waits up to five seconds until the receiver closes it,
as sd_notify_barrier(3) does. Use it where podman may exit after
notifying: MAINPID (and READY) after starting a container, READY once it
is healthy, and the fast path of kube play. A failed barrier is only
logged at debug level since the message itself was sent.

Also parse only the received control message in the proxy's listener.
It parsed the whole buffer, failed, and never closed the barrier fds.

The sdnotify system tests now receive on the notify socket with a new
podman-testing sd-notify-listen command instead of socat. socat keeps
the fds it receives, so every barrier ran into its timeout; the new
command closes them right away. BARRIER=1 is not logged.

Fixes: #29998
Signed-off-by: Tobias Wiesing <tobias@hueske.digital>
2026-10-21 15:29:10 +02:00

138 lines
3.6 KiB
Go

//go:build !windows
package notifyproxy
import (
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/coreos/go-systemd/v22/daemon"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
// Helper function to send the specified message over the socket of the proxy.
func sendMessage(t *testing.T, proxy *NotifyProxy, message string) {
err := SendMessage(proxy.SocketPath(), message)
require.NoError(t, err)
}
func TestSendMessageWaitsForBarrier(t *testing.T) {
socketPath := filepath.Join(t.TempDir(), "notify.sock")
conn, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: socketPath, Net: "unixgram"})
require.NoError(t, err)
defer conn.Close()
sent := make(chan error, 1)
go func() {
sent <- SendMessageWithBarrier(socketPath, daemon.SdNotifyReady)
}()
require.NoError(t, conn.SetReadDeadline(time.Now().Add(_notifyBarrierTimeout)))
buf := make([]byte, _notifyBufferMax)
oob := make([]byte, unix.CmsgSpace(4))
n, err := conn.Read(buf)
require.NoError(t, err)
require.Equal(t, daemon.SdNotifyReady, string(buf[:n]))
n, oobn, _, _, err := conn.ReadMsgUnix(buf, oob)
require.NoError(t, err)
scms, err := unix.ParseSocketControlMessage(oob[:oobn])
require.NoError(t, err)
require.Len(t, scms, 1)
fds, err := unix.ParseUnixRights(&scms[0])
require.NoError(t, err)
barriers := make([]*os.File, len(fds))
for i, fd := range fds {
barriers[i] = os.NewFile(uintptr(fd), "barrier")
defer barriers[i].Close()
}
require.Len(t, barriers, 1)
require.Equal(t, _notifyBarrierMsg, string(buf[:n]))
select {
case err := <-sent:
t.Fatalf("SendMessage returned before the barrier was closed: %v", err)
default:
}
require.NoError(t, barriers[0].Close())
select {
case err := <-sent:
require.NoError(t, err)
case <-time.After(_notifyBarrierTimeout):
t.Fatal("SendMessage did not return after the barrier was closed")
}
}
func TestSendMessageWithoutBarrierSupport(t *testing.T) {
socketPath := filepath.Join(t.TempDir(), "notify.sock")
conn, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: socketPath, Net: "unixgram"})
require.NoError(t, err)
defer conn.Close()
// Nobody reads the socket, so the barrier fd is never closed.
start := time.Now()
require.NoError(t, SendMessageWithBarrier(socketPath, daemon.SdNotifyReady))
require.GreaterOrEqual(t, time.Since(start), _notifyBarrierTimeout)
}
func TestProxyClosesBarrierFd(t *testing.T) {
proxy, err := New("")
require.NoError(t, err)
defer proxy.Close()
start := time.Now()
require.NoError(t, SendMessageWithBarrier(proxy.SocketPath(), "foo"))
require.Less(t, time.Since(start), _notifyBarrierTimeout)
}
func TestNotifyProxy(t *testing.T) {
proxy, err := New("")
require.NoError(t, err)
require.FileExists(t, proxy.SocketPath())
require.NoError(t, proxy.Close())
require.NoFileExists(t, proxy.SocketPath())
}
func TestWaitAndClose(t *testing.T) {
proxy, err := New("")
require.NoError(t, err)
require.FileExists(t, proxy.SocketPath())
ch := make(chan error)
defer func() {
err := proxy.Close()
require.NoError(t, err, "proxy should close successfully")
}()
go func() {
ch <- proxy.Wait()
}()
sendMessage(t, proxy, "foo\n")
time.Sleep(250 * time.Millisecond)
select {
case err := <-ch:
t.Fatalf("Should still be waiting but received %v", err)
default:
}
sendMessage(t, proxy, daemon.SdNotifyReady+"\nsomething else\n")
done := func() bool {
for i := range 10 {
select {
case err := <-ch:
require.NoError(t, err, "Waiting should succeed")
return true
default:
time.Sleep(time.Duration(i*250) * time.Millisecond)
}
}
return false
}()
require.True(t, done, "READY MESSAGE SHOULD HAVE ARRIVED")
}