diff --git a/cmd/image-builder/build.go b/cmd/image-builder/build.go index d07316f..bddc9ad 100644 --- a/cmd/image-builder/build.go +++ b/cmd/image-builder/build.go @@ -14,6 +14,7 @@ type buildOptions struct { StoreDir string WriteManifest bool + WriteBuildlog bool } func buildImage(pbar progress.ProgressBar, res *imagefilter.Result, osbuildManifest []byte, opts *buildOptions) error { @@ -31,5 +32,19 @@ func buildImage(pbar progress.ProgressBar, res *imagefilter.Result, osbuildManif } } - return progress.RunOSBuild(pbar, osbuildManifest, opts.StoreDir, opts.OutputDir, res.ImgType.Exports(), nil) + osbuildOpts := &progress.OSBuildOptions{ + StoreDir: opts.StoreDir, + OutputDir: opts.OutputDir, + } + if opts.WriteBuildlog { + p := filepath.Join(opts.OutputDir, fmt.Sprintf("%s.buildlog", outputNameFor(res))) + f, err := os.Create(p) + if err != nil { + return err + } + defer f.Close() + + osbuildOpts.BuildLog = f + } + return progress.RunOSBuild(pbar, osbuildManifest, res.ImgType.Exports(), osbuildOpts) } diff --git a/cmd/image-builder/main.go b/cmd/image-builder/main.go index 33fe4b0..00c84b3 100644 --- a/cmd/image-builder/main.go +++ b/cmd/image-builder/main.go @@ -207,6 +207,11 @@ func cmdBuild(cmd *cobra.Command, args []string) error { if err != nil { return err } + withBuildlog, err := cmd.Flags().GetBool("with-buildlog") + if err != nil { + return err + } + pbar, err := progressFromCmd(cmd) if err != nil { return err @@ -257,6 +262,7 @@ func cmdBuild(cmd *cobra.Command, args []string) error { OutputDir: outputDir, StoreDir: cacheDir, WriteManifest: withManifest, + WriteBuildlog: withBuildlog, } pbar.SetPulseMsgf("Image building step") if err := buildImage(pbar, res, mf.Bytes(), buildOpts); err != nil { @@ -379,6 +385,7 @@ operating systems like Fedora, CentOS and RHEL with easy customizations support. } buildCmd.Flags().AddFlagSet(manifestCmd.Flags()) buildCmd.Flags().Bool("with-manifest", false, `export osbuild manifest`) + buildCmd.Flags().Bool("with-buildlog", false, `export osbuild buildlog`) // XXX: add --rpmmd cache too and put under /var/cache/image-builder/dnf buildCmd.Flags().String("cache", "/var/cache/image-builder/store", `osbuild directory to cache intermediate build artifacts"`) // XXX: add "--verbose" here, similar to how bib is doing this diff --git a/cmd/image-builder/main_test.go b/cmd/image-builder/main_test.go index 5ae0e74..d5d8f07 100644 --- a/cmd/image-builder/main_test.go +++ b/cmd/image-builder/main_test.go @@ -384,6 +384,9 @@ func TestBuildIntegrationArgs(t *testing.T) { }, { []string{"--with-manifest"}, []string{"centos-9-qcow2-x86_64.osbuild-manifest.json"}, + }, { + []string{"--with-buildlog"}, + []string{"centos-9-qcow2-x86_64.buildlog"}, }, { []string{"--with-sbom"}, []string{"centos-9-qcow2-x86_64.buildroot-build.spdx.json", @@ -416,7 +419,7 @@ func TestBuildIntegrationArgs(t *testing.T) { defer fakeOsbuildCmd.Restore() err := main.Run() - assert.NoError(t, err) + require.NoError(t, err) // ensure output dir override works osbuildCall := fakeOsbuildCmd.Calls()[0] @@ -441,6 +444,7 @@ cat - > "$0".stdin echo "error on stdout" >&2 echo "error on stderr" +sleep 0.1 >&3 echo '{"message": "osbuild-stage-output"}' exit 1 ` @@ -471,12 +475,61 @@ func TestBuildIntegrationErrorsProgressVerbose(t *testing.T) { stdout, stderr := testutil.CaptureStdio(t, func() { err = main.Run() }) - assert.EqualError(t, err, "running osbuild failed: exit status 1") + assert.EqualError(t, err, "error running osbuild: exit status 1") assert.Contains(t, stdout, "error on stdout\n") assert.Contains(t, stderr, "error on stderr\n") } +func TestBuildIntegrationErrorsProgressVerboseWithBuildlog(t *testing.T) { + if testing.Short() { + t.Skip("manifest generation takes a while") + } + if !hasDepsolveDnf() { + t.Skip("no osbuild-depsolve-dnf binary found") + } + + restore := main.MockNewRepoRegistry(testrepos.New) + defer restore() + + outputDir := t.TempDir() + restore = main.MockOsArgs([]string{ + "build", + "qcow2", + "--distro", "centos-9", + "--progress=verbose", + "--with-buildlog", + "--output-dir", outputDir, + }) + defer restore() + + failingOsbuild := `#!/bin/sh +cat - > "$0".stdin +echo "error on stdout" +>&2 echo "error on stderr" +exit 1 +` + fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", failingOsbuild) + defer fakeOsbuildCmd.Restore() + + var err error + stdout, _ := testutil.CaptureStdio(t, func() { + err = main.Run() + }) + assert.EqualError(t, err, "error running osbuild: exit status 1") + + // when the buildlog is used we do not get the direct output of + // osbuild on stderr, to avoid races everything goes via stdout + assert.Contains(t, stdout, "error on stdout\n") + assert.Contains(t, stdout, "error on stderr\n") + + buildLog, err := os.ReadFile(filepath.Join(outputDir, "centos-9-qcow2-x86_64.buildlog")) + assert.NoError(t, err) + assert.Equal(t, string(buildLog), `error on stdout +error on stderr +`) +} + func TestBuildIntegrationErrorsProgressTerm(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") @@ -488,30 +541,52 @@ func TestBuildIntegrationErrorsProgressTerm(t *testing.T) { restore := main.MockNewRepoRegistry(testrepos.New) defer restore() - restore = main.MockOsArgs([]string{ - "build", - "qcow2", - "--distro", "centos-9", - "--progress=term", - }) - defer restore() + for _, withBuildlog := range []bool{false, true} { + t.Run(fmt.Sprintf("with buildlog %v", withBuildlog), func(t *testing.T) { + outputDir := t.TempDir() + cmd := []string{ + "build", + "qcow2", + "--distro", "centos-9", + "--progress=term", + "--output-dir", outputDir, + } + if withBuildlog { + cmd = append(cmd, "--with-buildlog") + } + restore = main.MockOsArgs(cmd) + defer restore() - fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", failingOsbuild) - defer fakeOsbuildCmd.Restore() + fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", failingOsbuild) + defer fakeOsbuildCmd.Restore() - var err error - stdout, stderr := testutil.CaptureStdio(t, func() { - err = main.Run() - }) - assert.EqualError(t, err, `error running osbuild: exit status 1 + var err error + stdout, stderr := testutil.CaptureStdio(t, func() { + err = main.Run() + }) + assert.EqualError(t, err, `error running osbuild: exit status 1 BuildLog: osbuild-stage-output Output: error on stdout error on stderr `) - assert.NotContains(t, stdout, "error on stdout") - assert.NotContains(t, stderr, "error on stderr") + assert.NotContains(t, stdout, "error on stdout") + assert.NotContains(t, stderr, "error on stderr") + + if withBuildlog { + buildLog, err := os.ReadFile(filepath.Join(outputDir, "centos-9-qcow2-x86_64.buildlog")) + assert.NoError(t, err) + assert.Equal(t, string(buildLog), `error on stdout +error on stderr +osbuild-stage-output +`) + } else { + _, err := os.Stat(filepath.Join(outputDir, "centos-9-qcow2-x86_64.buildlog")) + assert.True(t, os.IsNotExist(err)) + } + }) + } } func TestManifestIntegrationWithSBOMWithOutputDir(t *testing.T) {