diff --git a/cmd/image-builder/build.go b/cmd/image-builder/build.go index 5b9926b..c3cf145 100644 --- a/cmd/image-builder/build.go +++ b/cmd/image-builder/build.go @@ -5,14 +5,15 @@ import ( "github.com/osbuild/images/pkg/osbuild" ) -func buildImage(res *imagefilter.Result, osbuildManifest []byte, osbuildStoreDir string) error { - // XXX: support output dir via commandline - // XXX2: support output filename via commandline (c.f. +func buildImage(res *imagefilter.Result, osbuildManifest []byte, osbuildStoreDir, outputDir string) error { + // XXX: support output filename via commandline (c.f. // https://github.com/osbuild/images/pull/1039) - jobOutputDir := outputDirFor(res) + if outputDir == "" { + outputDir = outputDirFor(res) + } // XXX: support stremaing via images/pkg/osbuild/monitor.go - _, err := osbuild.RunOSBuild(osbuildManifest, osbuildStoreDir, jobOutputDir, res.ImgType.Exports(), nil, nil, false, osStderr) + _, err := osbuild.RunOSBuild(osbuildManifest, osbuildStoreDir, outputDir, res.ImgType.Exports(), nil, nil, false, osStderr) return err } diff --git a/cmd/image-builder/main.go b/cmd/image-builder/main.go index 15bb6c7..1c5f8aa 100644 --- a/cmd/image-builder/main.go +++ b/cmd/image-builder/main.go @@ -89,6 +89,10 @@ func cmdManifestWrapper(cmd *cobra.Command, args []string, w io.Writer, archChec if err != nil { return nil, err } + outputDir, err := cmd.Flags().GetString("output-dir") + if err != nil { + return nil, err + } ostreeImgOpts, err := ostreeImageOptions(cmd) if err != nil { return nil, err @@ -130,6 +134,7 @@ func cmdManifestWrapper(cmd *cobra.Command, args []string, w io.Writer, archChec } opts := &manifestOptions{ + OutputDir: outputDir, BlueprintPath: blueprintPath, Ostree: ostreeImgOpts, RpmDownloader: rpmDownloader, @@ -149,6 +154,10 @@ func cmdBuild(cmd *cobra.Command, args []string) error { if err != nil { return err } + outputDir, err := cmd.Flags().GetString("output-dir") + if err != nil { + return err + } var mf bytes.Buffer // XXX: check env here, i.e. if user is root and osbuild is installed @@ -162,7 +171,7 @@ func cmdBuild(cmd *cobra.Command, args []string) error { return err } - return buildImage(res, mf.Bytes(), storeDir) + return buildImage(res, mf.Bytes(), storeDir, outputDir) } func run() error { @@ -181,6 +190,7 @@ operating sytsems like centos and RHEL with easy customizations support.`, SilenceErrors: true, } rootCmd.PersistentFlags().String("datadir", "", `Override the default data direcotry for e.g. custom repositories/*.json data`) + rootCmd.PersistentFlags().String("output-dir", "", `Put output into the specified direcotry`) rootCmd.PersistentFlags().StringArray("extra-artifacts", nil, `Export extra artifacts to the output dir (e.g. "sbom")`) rootCmd.SetOut(osStdout) rootCmd.SetErr(osStderr) diff --git a/cmd/image-builder/main_test.go b/cmd/image-builder/main_test.go index 8b16e1b..5023574 100644 --- a/cmd/image-builder/main_test.go +++ b/cmd/image-builder/main_test.go @@ -359,6 +359,41 @@ func TestBuildIntegrationHappy(t *testing.T) { assert.Contains(t, string(manifest), `"image":{"name":"registry.gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/fedora-minimal"`) } +func TestBuildIntegrationSwitchOutputDir(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() + + tmpdir := t.TempDir() + restore = main.MockOsArgs([]string{ + "build", + "qcow2", + "--distro", "centos-9", + "--store", tmpdir, + "--output-dir", "some-output-dir", + }) + defer restore() + + script := `cat - > "$0".stdin` + fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", script) + defer fakeOsbuildCmd.Restore() + + err := main.Run() + assert.NoError(t, err) + + // ensure output dir override works + osbuildCall := fakeOsbuildCmd.Calls()[0] + outputDirPos := slices.Index(osbuildCall, "--output-directory") + assert.True(t, outputDirPos > -1) + assert.Equal(t, "some-output-dir", osbuildCall[outputDirPos+1]) +} + func TestBuildIntegrationErrors(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") @@ -399,3 +434,40 @@ exit 1 // osbuild-exec.go) assert.Equal(t, "error on stderr\n", fakeStderr.String()) } + +func TestManifestIntegrationExtraArtifactsSBOMWithOutputDir(t *testing.T) { + if testing.Short() { + t.Skip("manifest generation takes a while") + } + if !hasDepsolveDnf() { + t.Skip("no osbuild-depsolve-dnf binary found") + } + outputDir := filepath.Join(t.TempDir(), "output-dir") + + restore := main.MockNewRepoRegistry(testrepos.New) + defer restore() + + restore = main.MockOsArgs([]string{ + "manifest", + "qcow2", + "--arch=x86_64", + "--distro=centos-9", + fmt.Sprintf("--blueprint=%s", makeTestBlueprint(t, testBlueprint)), + "--extra-artifacts=sbom", + "--output-dir", outputDir, + }) + defer restore() + + var fakeStdout bytes.Buffer + restore = main.MockOsStdout(&fakeStdout) + defer restore() + + err := main.Run() + assert.NoError(t, err) + + sboms, err := filepath.Glob(filepath.Join(outputDir, "*.spdx.json")) + assert.NoError(t, err) + assert.Equal(t, len(sboms), 2) + assert.Equal(t, filepath.Join(outputDir, "centos-9-qcow2-x86_64.buildroot-build.spdx.json"), sboms[0]) + assert.Equal(t, filepath.Join(outputDir, "centos-9-qcow2-x86_64.image-os.spdx.json"), sboms[1]) +} diff --git a/cmd/image-builder/manifest.go b/cmd/image-builder/manifest.go index 51001d5..414fdd3 100644 --- a/cmd/image-builder/manifest.go +++ b/cmd/image-builder/manifest.go @@ -16,6 +16,7 @@ import ( ) type manifestOptions struct { + OutputDir string BlueprintPath string Ostree *ostree.ImageOptions RpmDownloader osbuild.RpmDownloader @@ -46,7 +47,10 @@ func generateManifest(dataDir string, img *imagefilter.Result, output io.Writer, RpmDownloader: opts.RpmDownloader, } if slices.Contains(opts.ExtraArtifacts, "sbom") { - outputDir := outputDirFor(img) + outputDir := opts.OutputDir + if outputDir == "" { + outputDir = outputDirFor(img) + } if err := os.MkdirAll(outputDir, 0755); err != nil { return err }