From f49b4b3f8465d13f91d73ecf755657a1bfbc76c4 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 29 Jun 2026 23:54:15 -0700 Subject: [PATCH] fix: emit quadlet generator logs to stderr even when kmsg succeeds Logf wrote to /dev/kmsg and only fell back to stderr when the kmsg write failed or -dryrun was set. As root the kmsg write succeeds, so nothing reached stderr and `sudo systemd-analyze --generators verify` (which captures stderr, not kmsg) showed no diagnostics, while the same command without sudo did. Write to /dev/kmsg best-effort for early-boot/journal capture AND always copy the same line to stderr so verify and interactive sudo runs surface the messages. Rootless behavior is unchanged. Fixes: #28888 Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> --- cmd/quadlet/main.go | 8 +-- cmd/quadlet/main_test.go | 115 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/cmd/quadlet/main.go b/cmd/quadlet/main.go index fe78d1f92dd..936d64771f5 100644 --- a/cmd/quadlet/main.go +++ b/cmd/quadlet/main.go @@ -70,10 +70,10 @@ func Logf(format string, a ...any) { s := fmt.Sprintf(format, a...) line := fmt.Sprintf("quadlet-generator[%d]: %s", os.Getpid(), s) - if !logToKmsg(line) || dryRunFlag { - fmt.Fprintf(os.Stderr, "%s\n", line) - os.Stderr.Sync() - } + logToKmsg(line) + + fmt.Fprintf(os.Stderr, "%s\n", line) + os.Stderr.Sync() } var debugEnabled = false diff --git a/cmd/quadlet/main_test.go b/cmd/quadlet/main_test.go index a9f137e8611..2c3d946cbe2 100644 --- a/cmd/quadlet/main_test.go +++ b/cmd/quadlet/main_test.go @@ -3,11 +3,126 @@ package main import ( + "fmt" + "io" + "os" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func TestLogfWritesToStderrWhenKmsgUnavailable(t *testing.T) { + restoreLogGlobals(t) + noKmsg = true + kmsgFile = nil + dryRunFlag = false + + stderr := captureStderr(t, func() { + Logf("kmsg unavailable") + }) + + assert.Equal(t, expectedLogLine("kmsg unavailable")+"\n", stderr) +} + +func TestLogfWritesToStderrWhenKmsgSucceeds(t *testing.T) { + restoreLogGlobals(t) + noKmsg = false + dryRunFlag = false + + tmpFile, err := os.CreateTemp(t.TempDir(), "kmsg") + require.NoError(t, err) + t.Cleanup(func() { + tmpFile.Close() + }) + kmsgFile = tmpFile + + stderr := captureStderr(t, func() { + Logf("kmsg succeeds") + }) + + line := expectedLogLine("kmsg succeeds") + assert.Equal(t, line+"\n", stderr) + + _, err = tmpFile.Seek(0, io.SeekStart) + require.NoError(t, err) + kmsg, err := io.ReadAll(tmpFile) + require.NoError(t, err) + assert.Equal(t, line, string(kmsg)) +} + +func TestLogfWritesToStderrInDryRun(t *testing.T) { + restoreLogGlobals(t) + noKmsg = true + kmsgFile = nil + dryRunFlag = true + + stderr := captureStderr(t, func() { + Logf("dry run") + }) + + assert.Equal(t, expectedLogLine("dry run")+"\n", stderr) +} + +func TestLogfWritesToStderrWhenKmsgWriteFails(t *testing.T) { + restoreLogGlobals(t) + noKmsg = false + dryRunFlag = false + + tmpFile, err := os.CreateTemp(t.TempDir(), "kmsg") + require.NoError(t, err) + require.NoError(t, tmpFile.Close()) + kmsgFile = tmpFile + + stderr := captureStderr(t, func() { + Logf("kmsg write failure") + }) + + assert.Equal(t, expectedLogLine("kmsg write failure")+"\n", stderr) + assert.Nil(t, kmsgFile) +} + +func restoreLogGlobals(t *testing.T) { + t.Helper() + + oldNoKmsg := noKmsg + oldKmsgFile := kmsgFile + oldDryRunFlag := dryRunFlag + + t.Cleanup(func() { + noKmsg = oldNoKmsg + kmsgFile = oldKmsgFile + dryRunFlag = oldDryRunFlag + }) +} + +func captureStderr(t *testing.T, f func()) string { + t.Helper() + + oldStderr := os.Stderr + reader, writer, err := os.Pipe() + require.NoError(t, err) + + os.Stderr = writer + defer func() { + os.Stderr = oldStderr + }() + + f() + require.NoError(t, writer.Close()) + os.Stderr = oldStderr + + output, err := io.ReadAll(reader) + require.NoError(t, err) + require.NoError(t, reader.Close()) + + return string(output) +} + +func expectedLogLine(message string) string { + return fmt.Sprintf("quadlet-generator[%d]: %s", os.Getpid(), message) +} + func TestIsUnambiguousName(t *testing.T) { tests := []struct { input string