Skip to content

Commit a00d61b

Browse files
committed
progress: add new BuildLog option
This commit adds a new `BuildLog` option to the `OSBuildOptions` that can be used to generate a streamed buildlog (e.g. to a file or a websocket). This will be used in `ibcli` with a new `--with-buildlog` option.
1 parent 8e13ecf commit a00d61b

File tree

5 files changed

+210
-5
lines changed

5 files changed

+210
-5
lines changed

bib/pkg/progress/export_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ type (
1010
VerboseProgressBar = verboseProgressBar
1111
)
1212

13+
var (
14+
NewSyncedWriter = newSyncedWriter
15+
)
16+
17+
func MockOsStdout(w io.Writer) (restore func()) {
18+
saved := osStdout
19+
osStdout = func() io.Writer { return w }
20+
return func() {
21+
osStdout = saved
22+
}
23+
}
24+
1325
func MockOsStderr(w io.Writer) (restore func()) {
1426
saved := osStderr
1527
osStderr = func() io.Writer { return w }

bib/pkg/progress/progress.go

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"os/exec"
1010
"strings"
11+
"sync"
1112
"syscall"
1213
"time"
1314

@@ -32,6 +33,9 @@ var (
3233
// Used for testing, this must be a function (instead of the usual
3334
// "var osStderr = os.Stderr" so that higher level libraries can test
3435
// this code by replacing "os.Stderr", e.g. testutil.CaptureStdio()
36+
var osStdout = func() io.Writer {
37+
return os.Stdout
38+
}
3539
var osStderr = func() io.Writer {
3640
return os.Stderr
3741
}
@@ -321,6 +325,9 @@ type OSBuildOptions struct {
321325
StoreDir string
322326
OutputDir string
323327
ExtraEnv []string
328+
329+
// BuildLog writes the osbuild output to the given writer
330+
BuildLog io.Writer
324331
}
325332

326333
// XXX: merge variant back into images/pkg/osbuild/osbuild-exec.go
@@ -345,8 +352,52 @@ func RunOSBuild(pb ProgressBar, manifest []byte, exports []string, opts *OSBuild
345352
}
346353

347354
func runOSBuildNoProgress(pb ProgressBar, manifest []byte, exports []string, opts *OSBuildOptions) error {
348-
_, err := osbuild.RunOSBuild(manifest, opts.StoreDir, opts.OutputDir, exports, nil, opts.ExtraEnv, false, os.Stderr)
349-
return err
355+
var stdout, stderr io.Writer
356+
357+
var writeMu sync.Mutex
358+
if opts.BuildLog == nil {
359+
// No external build log requested and we won't need an
360+
// internal one because all output goes directly to
361+
// stdout/stderr. This is for maximum compatibility with
362+
// the existing bootc-image-builder in "verbose" mode
363+
// where stdout, stderr come directly from osbuild.
364+
stdout = osStdout()
365+
stderr = osStderr()
366+
} else {
367+
// There is a slight wrinkle here: when requesting a
368+
// buildlog we can no longer write to separate
369+
// stdout/stderr streams without being racy and give
370+
// potential out-of-order output (which is very bad
371+
// and confusing in a log). The reason is that if
372+
// cmd.Std{out,err} are different "go" will start two
373+
// go-routine to monitor/copy those are racy when both
374+
// stdout,stderr output happens close together
375+
// (TestRunOSBuildWithBuildlog demos that). We cannot
376+
// have our cake and eat it so here we need to combine
377+
// osbuilds stderr into our stdout.
378+
mw := newSyncedWriter(&writeMu, io.MultiWriter(osStdout(), opts.BuildLog))
379+
stdout = mw
380+
stderr = mw
381+
}
382+
383+
cmd := exec.Command(
384+
osbuildCmd,
385+
"--store", opts.StoreDir,
386+
"--output-directory", opts.OutputDir,
387+
"-",
388+
)
389+
for _, export := range exports {
390+
cmd.Args = append(cmd.Args, "--export", export)
391+
}
392+
393+
cmd.Env = append(os.Environ(), opts.ExtraEnv...)
394+
cmd.Stdin = bytes.NewBuffer(manifest)
395+
cmd.Stdout = stdout
396+
cmd.Stderr = stderr
397+
if err := cmd.Run(); err != nil {
398+
return fmt.Errorf("error running osbuild: %w", err)
399+
}
400+
return nil
350401
}
351402

352403
var osbuildCmd = "osbuild"
@@ -372,10 +423,20 @@ func runOSBuildWithProgress(pb ProgressBar, manifest []byte, exports []string, o
372423
}
373424

374425
var stdio bytes.Buffer
426+
var mw, buildLog io.Writer
427+
var writeMu sync.Mutex
428+
if opts.BuildLog != nil {
429+
mw = newSyncedWriter(&writeMu, io.MultiWriter(&stdio, opts.BuildLog))
430+
buildLog = newSyncedWriter(&writeMu, opts.BuildLog)
431+
} else {
432+
mw = &stdio
433+
buildLog = io.Discard
434+
}
435+
375436
cmd.Env = append(os.Environ(), opts.ExtraEnv...)
376437
cmd.Stdin = bytes.NewBuffer(manifest)
377-
cmd.Stdout = &stdio
378-
cmd.Stderr = &stdio
438+
cmd.Stdout = mw
439+
cmd.Stderr = mw
379440
cmd.ExtraFiles = []*os.File{wp}
380441

381442
osbuildStatus := osbuild.NewStatusScanner(rp)
@@ -425,12 +486,15 @@ func runOSBuildWithProgress(pb ProgressBar, manifest []byte, exports []string, o
425486
pb.SetMessagef(st.Message)
426487
}
427488

428-
// keep all messages/traces for better error reporting
489+
// keep internal log for error reporting, forward to
490+
// external build log
429491
if st.Message != "" {
430492
tracesMsgs = append(tracesMsgs, st.Message)
493+
fmt.Fprintln(buildLog, st.Message)
431494
}
432495
if st.Trace != "" {
433496
tracesMsgs = append(tracesMsgs, st.Trace)
497+
fmt.Fprintln(buildLog, st.Trace)
434498
}
435499
}
436500

bib/pkg/progress/progress_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,66 @@ done
200200
}
201201
assert.True(t, pathExists(signalDeliveredMarkerPath))
202202
}
203+
204+
func TestRunOSBuildWithBuildlogTerm(t *testing.T) {
205+
restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, `
206+
echo osbuild-stdout-output
207+
>&2 echo osbuild-stderr-output
208+
209+
# without the sleep this is racy as two different go routines poll
210+
# this does not matter (much) in practise because osbuild output and
211+
# stage output are using the syncedMultiWriter so output is not garbled
212+
sleep 0.1
213+
>&3 echo '{"message": "osbuild-stage-message"}'
214+
`))
215+
defer restore()
216+
217+
var fakeStdout, fakeStderr bytes.Buffer
218+
restore = progress.MockOsStdout(&fakeStdout)
219+
defer restore()
220+
restore = progress.MockOsStderr(&fakeStderr)
221+
defer restore()
222+
223+
pbar, err := progress.New("term")
224+
assert.NoError(t, err)
225+
226+
var buildLog bytes.Buffer
227+
opts := &progress.OSBuildOptions{
228+
BuildLog: &buildLog,
229+
}
230+
err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, opts)
231+
assert.NoError(t, err)
232+
expectedOutput := `osbuild-stdout-output
233+
osbuild-stderr-output
234+
osbuild-stage-message
235+
`
236+
assert.Equal(t, expectedOutput, buildLog.String())
237+
}
238+
239+
func TestRunOSBuildWithBuildlogVerbose(t *testing.T) {
240+
restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, `
241+
echo osbuild-stdout-output
242+
>&2 echo osbuild-stderr-output
243+
`))
244+
defer restore()
245+
246+
var fakeStdout, fakeStderr bytes.Buffer
247+
restore = progress.MockOsStdout(&fakeStdout)
248+
defer restore()
249+
restore = progress.MockOsStderr(&fakeStderr)
250+
defer restore()
251+
252+
pbar, err := progress.New("verbose")
253+
assert.NoError(t, err)
254+
255+
var buildLog bytes.Buffer
256+
opts := &progress.OSBuildOptions{
257+
BuildLog: &buildLog,
258+
}
259+
err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, opts)
260+
assert.NoError(t, err)
261+
expectedOutput := `osbuild-stdout-output
262+
osbuild-stderr-output
263+
`
264+
assert.Equal(t, expectedOutput, buildLog.String())
265+
}

bib/pkg/progress/syncwriter.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package progress
2+
3+
import (
4+
"io"
5+
"sync"
6+
)
7+
8+
type syncedWriter struct {
9+
mu *sync.Mutex
10+
w io.Writer
11+
}
12+
13+
func newSyncedWriter(mu *sync.Mutex, w io.Writer) io.Writer {
14+
return &syncedWriter{mu: mu, w: w}
15+
}
16+
17+
func (sw *syncedWriter) Write(p []byte) (n int, err error) {
18+
sw.mu.Lock()
19+
defer sw.mu.Unlock()
20+
21+
return sw.w.Write(p)
22+
}

bib/pkg/progress/syncwriter_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package progress_test
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"strings"
8+
"sync"
9+
"testing"
10+
"time"
11+
12+
"github.com/stretchr/testify/assert"
13+
14+
"github.com/osbuild/bootc-image-builder/bib/pkg/progress"
15+
)
16+
17+
func TestSyncWriter(t *testing.T) {
18+
var mu sync.Mutex
19+
var buf bytes.Buffer
20+
var wg sync.WaitGroup
21+
22+
for id := 0; id < 100; id++ {
23+
wg.Add(1)
24+
w := progress.NewSyncedWriter(&mu, &buf)
25+
go func(id int) {
26+
defer wg.Done()
27+
for i := 0; i < 500; i++ {
28+
fmt.Fprintln(w, strings.Repeat(fmt.Sprintf("%v", id%10), 60))
29+
time.Sleep(10 * time.Nanosecond)
30+
}
31+
}(id)
32+
}
33+
wg.Wait()
34+
35+
scanner := bufio.NewScanner(&buf)
36+
for {
37+
if !scanner.Scan() {
38+
break
39+
}
40+
line := scanner.Text()
41+
assert.True(t, len(line) == 60, fmt.Sprintf("len %v: line: %v", len(line), line))
42+
}
43+
assert.NoError(t, scanner.Err())
44+
}

0 commit comments

Comments
 (0)