Skip to content

Commit 97efd0f

Browse files
committed
cmd/osbuild-worker-executor: use JSONSeqMonitor
1 parent 3014f65 commit 97efd0f

File tree

3 files changed

+94
-48
lines changed

3 files changed

+94
-48
lines changed

cmd/osbuild-worker-executor/export_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"fmt"
45
"os"
56
"path/filepath"
67
"testing"
@@ -15,16 +16,15 @@ var (
1516
func MockOsbuildBinary(t *testing.T, new string) (restore func()) {
1617
t.Helper()
1718

18-
saved := osbuildBinary
19-
2019
tmpdir := t.TempDir()
21-
osbuildBinary = filepath.Join(tmpdir, "fake-osbuild")
2220
/* #nosec G306 */
23-
if err := os.WriteFile(osbuildBinary, []byte(new), 0755); err != nil {
21+
if err := os.WriteFile(filepath.Join(tmpdir, "osbuild"), []byte(new), 0755); err != nil {
2422
t.Fatal(err)
2523
}
24+
path := os.Getenv("PATH")
25+
os.Setenv("PATH", fmt.Sprintf("%s:%s", tmpdir, path))
2626

2727
return func() {
28-
osbuildBinary = saved
28+
os.Setenv("PATH", path)
2929
}
3030
}

cmd/osbuild-worker-executor/handler_build.go

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"archive/tar"
5+
"bufio"
56
"encoding/json"
67
"errors"
78
"fmt"
@@ -15,11 +16,12 @@ import (
1516
"golang.org/x/exp/slices"
1617

1718
"github.com/sirupsen/logrus"
19+
20+
"github.com/osbuild/images/pkg/osbuild"
1821
)
1922

2023
var (
2124
supportedBuildContentTypes = []string{"application/x-tar"}
22-
osbuildBinary = "osbuild"
2325
)
2426

2527
var (
@@ -39,63 +41,96 @@ func (wf *writeFlusher) Write(p []byte) (n int, err error) {
3941
return n, err
4042
}
4143

42-
func runOsbuild(logger *logrus.Logger, buildDir string, control *controlJSON, output io.Writer) (string, error) {
43-
flusher, ok := output.(http.Flusher)
44-
if !ok {
45-
return "", fmt.Errorf("cannot stream the output")
46-
}
47-
// stream output over http
48-
wf := writeFlusher{w: output, flusher: flusher}
44+
func runOSBuild(logger *logrus.Logger, buildDir string, control *controlJSON, output io.Writer) (string, error) {
4945
// note that the "filepath.Clean()" is here for gosec:G304
5046
logfPath := filepath.Clean(filepath.Join(buildDir, "build.log"))
51-
// and also write to our internal log
5247
logf, err := os.Create(logfPath)
5348
if err != nil {
5449
return "", fmt.Errorf("cannot create log file: %v", err)
5550
}
5651
defer logf.Close()
5752

53+
manifest, err := os.ReadFile(filepath.Join(buildDir, "manifest.json"))
54+
if err != nil {
55+
return "", fmt.Errorf("cannot read manifest file: %w", err)
56+
}
57+
5858
// use multi writer to get same output for stream and log
59-
mw := io.MultiWriter(&wf, logf)
6059
outputDir := filepath.Join(buildDir, "output")
6160
storeDir := filepath.Join(buildDir, "osbuild-store")
62-
cmd := exec.Command(osbuildBinary)
63-
cmd.Stdout = mw
64-
cmd.Stderr = mw
65-
for _, exp := range control.Exports {
66-
cmd.Args = append(cmd.Args, []string{"--export", exp}...)
61+
62+
// MonitorFile needs an *os.File:
63+
// MonitorFile -> pipe -> bufio reader -> writeFlusher
64+
rPipe, wPipe, err := os.Pipe()
65+
if err != nil {
66+
return "", fmt.Errorf("cannot create pipe for monitor file: %w", err)
6767
}
68-
cmd.Env = append(cmd.Env, control.Environments...)
69-
cmd.Args = append(cmd.Args, []string{"--output-dir", outputDir}...)
70-
cmd.Args = append(cmd.Args, []string{"--store", storeDir}...)
71-
cmd.Args = append(cmd.Args, "--json")
72-
cmd.Args = append(cmd.Args, filepath.Join(buildDir, "manifest.json"))
68+
defer rPipe.Close()
69+
defer wPipe.Close()
70+
71+
cmd := osbuild.NewOSBuildCmd(manifest, &osbuild.OSBuildOptions{
72+
StoreDir: storeDir,
73+
OutputDir: outputDir,
74+
Exports: control.Exports,
75+
ExtraEnv: control.Environments,
76+
BuildLog: logf,
77+
Stdout: os.Stdout,
78+
Monitor: osbuild.MonitorJSONSeq,
79+
MonitorFile: wPipe,
80+
})
7381
if err := cmd.Start(); err != nil {
7482
return "", err
7583
}
84+
flusher, ok := output.(http.Flusher)
85+
if !ok {
86+
return "", fmt.Errorf("cannot stream the output, output needs to be http.Flusher")
87+
}
88+
89+
wf := writeFlusher{w: output, flusher: flusher}
90+
reader := bufio.NewReader(rPipe)
91+
wPipe.Close()
92+
for {
93+
line, err := reader.ReadBytes('\n')
94+
if err != nil && err != io.EOF {
95+
return "", fmt.Errorf("cannot read bytes from monitor pipe: %w", err)
96+
}
97+
98+
// handle the case where we have a (final) line without a \n, here we
99+
// get a EOF but still have line content to process before we can exit
100+
// Note that we have not seen this in practise, its for compat with the
101+
// previous bufio.Scanner implementation that had this behavior.
102+
if err == io.EOF && len(line) == 0 {
103+
break
104+
}
105+
106+
_, err = wf.Write(line)
107+
if err != nil {
108+
return "", fmt.Errorf("cannot write bytes to writeFlusher: %w", err)
109+
}
110+
}
76111

77112
if err := cmd.Wait(); err != nil {
78113
// we cannot use "http.Error()" here because the http
79114
// header was already set to "201" when we started streaming
80-
_, _ = mw.Write([]byte(fmt.Sprintf("cannot run osbuild: %v", err)))
115+
_, _ = wf.Write([]byte(fmt.Sprintf("cannot run osbuild: %v", err)))
81116
return "", err
82117
}
83118

84119
// the result is put into a tar because we get sparse file support for free this way
85120
// #nosec G204
86-
cmd = exec.Command(
121+
tarCmd := exec.Command(
87122
"tar",
88123
"--exclude=output/output.tar",
89124
"-Scf",
90125
filepath.Join(outputDir, "output.tar"),
91126
"output",
92127
)
93-
cmd.Dir = buildDir
94-
out, err := cmd.CombinedOutput()
128+
tarCmd.Dir = buildDir
129+
out, err := tarCmd.CombinedOutput()
95130
if err != nil {
96131
err = fmt.Errorf("cannot tar output directory: %w, output:\n%s", err, out)
97132
logger.Errorf("%v", err)
98-
_, _ = mw.Write([]byte(err.Error()))
133+
_, _ = wf.Write([]byte(err.Error()))
99134
return "", err
100135
}
101136
if len(out) > 0 {
@@ -291,12 +326,12 @@ func handleBuild(logger *logrus.Logger, config *Config) http.Handler {
291326

292327
// run osbuild and stream the output to the client
293328
buildResult := newBuildResult(config)
294-
_, err = runOsbuild(logger, buildDir, control, w)
329+
_, err = runOSBuild(logger, buildDir, control, w)
295330
if werr := buildResult.Mark(err); werr != nil {
296331
logger.Errorf("cannot write result file %v", werr)
297332
}
298333
if err != nil {
299-
logger.Errorf("canot run osbuild: %v", err)
334+
logger.Errorf("cannot run osbuild: %v", err)
300335
return
301336
}
302337
},

cmd/osbuild-worker-executor/handler_build_test.go

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,18 @@ func TestBuildIntegration(t *testing.T) {
8484
baseURL, baseBuildDir, loggerHook := runTestServer(t)
8585
endpoint := baseURL + "api/v1/build"
8686

87-
// osbuild is called with --export tree and then the manifest.json
8887
restore := main.MockOsbuildBinary(t, fmt.Sprintf(`#!/bin/sh -e
88+
# make sure the monitor is setup correctly
89+
>&3 echo '^^{"message": "osbuild-stage-message 1"}'
90+
>&3 echo '^^{"message": "osbuild-stage-message 2"}'
91+
>&3 echo '^^{"message": "osbuild-stage-message 3"}'
92+
8993
# echo our inputs for the test to validate
90-
echo fake-osbuild "$1" "$2" "$3" "$4" "$5" "$6" "$7"
94+
echo osbuild $@
9195
echo ---
92-
cat "$8"
96+
# stdin
97+
cat -
98+
echo
9399
94100
test "$MY" = "env"
95101
@@ -104,18 +110,23 @@ echo "fake-build-result" > %[1]s/build/output/image/disk.img
104110
assert.NoError(t, err)
105111
defer func() { _, _ = io.ReadAll(rsp.Body) }()
106112
defer rsp.Body.Close()
107-
108113
assert.Equal(t, http.StatusCreated, rsp.StatusCode)
109-
reader := bufio.NewReader(rsp.Body)
110114

111-
// check that we get the output of osbuild streamed to us
112-
expectedContent := fmt.Sprintf(`fake-osbuild --export tree --output-dir %[1]s/build/output --store %[1]s/build/osbuild-store --json
113-
---
114-
{"fake": "manifest"}`, baseBuildDir)
115+
// check that we get the monitor output of osbuild streamed to us
116+
expectedMonitorContent := `^^{"message": "osbuild-stage-message 1"}
117+
^^{"message": "osbuild-stage-message 2"}
118+
^^{"message": "osbuild-stage-message 3"}
119+
`
120+
reader := bufio.NewReader(rsp.Body)
115121
content, err := io.ReadAll(reader)
116122
assert.NoError(t, err)
117-
assert.Equal(t, expectedContent, string(content))
123+
assert.Equal(t, expectedMonitorContent, string(content))
124+
118125
// check log too
126+
expectedContent := fmt.Sprintf(`osbuild --store %[1]s/build/osbuild-store --output-directory %[1]s/build/output --cache-max-size=21474836480 - --export tree --monitor=JSONSeqMonitor --monitor-fd=3
127+
---
128+
{"fake": "manifest"}
129+
`, baseBuildDir)
119130
logFileContent, err := os.ReadFile(filepath.Join(baseBuildDir, "build/build.log"))
120131
assert.NoError(t, err)
121132
assert.Equal(t, expectedContent, string(logFileContent))
@@ -245,10 +256,7 @@ exit 23
245256
reader := bufio.NewReader(rsp.Body)
246257
content, err := io.ReadAll(reader)
247258
assert.NoError(t, err)
248-
expectedContent := `err on stdout
249-
err on stderr
250-
cannot run osbuild: exit status 23`
251-
assert.Equal(t, expectedContent, string(content))
259+
assert.Equal(t, "cannot run osbuild: exit status 23", string(content))
252260

253261
// check that the result is an error and we get the log
254262
endpoint = baseURL + "api/v1/result/image/disk.img"
@@ -259,7 +267,10 @@ cannot run osbuild: exit status 23`
259267
reader = bufio.NewReader(rsp.Body)
260268
content, err = io.ReadAll(reader)
261269
assert.NoError(t, err)
262-
assert.Equal(t, "build failed\n"+expectedContent, string(content))
270+
assert.Equal(t, `build failed
271+
err on stdout
272+
err on stderr
273+
`, string(content))
263274
}
264275

265276
func TestBuildStreamsOutput(t *testing.T) {
@@ -269,7 +280,7 @@ func TestBuildStreamsOutput(t *testing.T) {
269280
restore := main.MockOsbuildBinary(t, fmt.Sprintf(`#!/bin/sh -e
270281
for i in $(seq 3); do
271282
# generate the exact timestamp of the output line
272-
echo "line-$i: $(date +'%%s.%%N')"
283+
>&3 echo "line-$i: $(date +'%%s.%%N')"
273284
sleep 0.2
274285
done
275286

0 commit comments

Comments
 (0)