From 0b6b8a7211ab6f2a30ba1158cd654da007f696c7 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Mon, 10 Aug 2026 14:31:27 +0200 Subject: [PATCH] executor: return error message from the journal Previously, the output of the systemd unit was only reported in bootc operator. Now, it is returned as part of the error when bootc switch fails. The output for the systemd unit is quite verbose, so we filter for the lines containing the errors tag. The full output will be reduced from: bootc stage failed: running bootc switch: Started bootc-operator-switch.service - [systemd-run] /usr/bin/bootc switch ghcr.io/bootc-dev/bink/node@sha256:9ce7d6d15b8558c226b4c41f3b27bf1722897b0c8a66c7a84a2877bca8d049f7. Switching from image ghcr.io/bootc-dev/bink/node:v1.35-fedora-44 to ghcr.io/bootc-dev/bink/node@sha256:9ce7d6d15b8558c226b4c41f3b27bf1722897b0c8a66c7a84a2877bca8d049f7 Fetching ostree-unverified-registry:ghcr.io/bootc-dev/bink/node@sha256:9ce7d6d15b8558c226b4c41f3b27bf1722897b0c8a66c7a84a2877bca8d049f7 error: Switching: Switching (ostree): Preparing import: Fetching manifest: Target image does not have ostree.bootable label bootc-operator-switch.service: Main process exited, code=exited, status=1/FAILURE bootc-operator-switch.service: Failed with result 'exit-code'. bootc-operator-switch.service: Consumed 210ms CPU time over 1.111s wall clock time, 155.2M memory peak.: exit status 1 to 'bootc stage failed: running bootc switch: error: Switching: Switching (ostree): Preparing import: Fetching manifest: Target image does not have ostree.bootable label: exit status 1' The full message in any case is present in the bootc daemon logs. Assisted-by: AI Signed-off-by: Alice Frosi --- internal/bootc/executor.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/internal/bootc/executor.go b/internal/bootc/executor.go index 3452930..64d1086 100644 --- a/internal/bootc/executor.go +++ b/internal/bootc/executor.go @@ -85,7 +85,10 @@ func (e *HostExecutor) Stage(ctx context.Context, image string) error { if ctx.Err() != nil { return ctx.Err() } - e.copyJournalUnitLogs(log, stageUnitName, cursor) + journalOutput := e.copyJournalUnitLogs(log, stageUnitName, cursor) + if journalOutput != "" { + return fmt.Errorf("running bootc switch: %s: %w", journalOutput, err) + } return fmt.Errorf("running bootc switch: %w", err) } return nil @@ -117,10 +120,10 @@ func (e *HostExecutor) journalCursor() string { return "" } -// copyJournalUnitLogs logs recent journal output from the given systemd unit. -// If cursor is non-empty, only entries after that cursor are shown; otherwise -// shows all entries. -func (e *HostExecutor) copyJournalUnitLogs(log logr.Logger, unit string, cursor string) { +// copyJournalUnitLogs logs recent journal output from the given systemd unit +// and returns the raw output. If cursor is non-empty, only entries after that +// cursor are shown; otherwise shows all entries. +func (e *HostExecutor) copyJournalUnitLogs(log logr.Logger, unit string, cursor string) string { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() args := []string{"journalctl", "-o", "cat", "--no-pager", @@ -132,13 +135,18 @@ func (e *HostExecutor) copyJournalUnitLogs(log logr.Logger, unit string, cursor out, err := e.nsenterCmd(ctx, args...).Output() if err != nil { log.Error(err, "Failed to read unit journal", "unit", unit) - return + return "" } + var errLines []string for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { if line != "" { log.Info(line, "unit", unit) + if strings.HasPrefix(line, "error:") { + errLines = append(errLines, line) + } } } + return strings.Join(errLines, "\n") } func (e *HostExecutor) Reboot(ctx context.Context) error {