From e5b3ccd6ed995683b094c7b01d4a173e93da409d Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 3 Dec 2024 10:17:47 +0100 Subject: [PATCH] cmd: add new `build` command This commit adds the `build` command. It takes the same flags as `manifest` and will build the specified image. --- README.md | 11 ++++- cmd/image-builder/build.go | 24 ++++++++++ cmd/image-builder/main.go | 58 +++++++++++++++++------ cmd/image-builder/main_test.go | 86 ++++++++++++++++++++++++++++++++++ cmd/image-builder/manifest.go | 30 ++++++++++++ 5 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 cmd/image-builder/build.go create mode 100644 cmd/image-builder/manifest.go diff --git a/README.md b/README.md index 2572219..1d3d4de 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ $ sudo dnf install osbuild osbuild-depsolve-dnf osbuild-composer (`osbuild-composer` is only needed to get the repository definitions and this dependency will go away soon). -## Example +## Examples To see the list of buildable images run: ```console @@ -37,6 +37,15 @@ rhel-10.0 type:ami arch:x86_64 ... ``` +To actually build an image run: +```console +$ sudo image-builder build qcow2 --distro centos-9 +... +``` +this will create a directory `centos-9-qcow2-x86_64` under which the +output is stored. + + It is possible to filter: ```console $ image-builder list-images --filter ami diff --git a/cmd/image-builder/build.go b/cmd/image-builder/build.go new file mode 100644 index 0000000..124755c --- /dev/null +++ b/cmd/image-builder/build.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + "path/filepath" + + "github.com/osbuild/images/pkg/imagefilter" + "github.com/osbuild/images/pkg/osbuild" +) + +func buildImage(res *imagefilter.Result, osbuildManifest []byte) error { + osbuildStoreDir := ".store" + // XXX: support output dir via commandline + // XXX2: support output filename via commandline (c.f. + // https://github.com/osbuild/images/pull/1039) + outputDir := "." + buildName := fmt.Sprintf("%s-%s-%s", res.Distro.Name(), res.ImgType.Name(), res.Arch.Name()) + jobOutputDir := filepath.Join(outputDir, buildName) + + // XXX: support stremaing via images/pkg/osbuild/monitor.go + _, err := osbuild.RunOSBuild(osbuildManifest, osbuildStoreDir, jobOutputDir, res.ImgType.Exports(), nil, nil, false, osStderr) + return err + +} diff --git a/cmd/image-builder/main.go b/cmd/image-builder/main.go index 39f24cb..182ce33 100644 --- a/cmd/image-builder/main.go +++ b/cmd/image-builder/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "fmt" "io" "os" @@ -9,9 +10,9 @@ import ( "github.com/spf13/cobra" "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/imagefilter" "github.com/osbuild/image-builder-cli/internal/blueprintload" - "github.com/osbuild/image-builder-cli/internal/manifestgen" ) var ( @@ -36,21 +37,21 @@ func cmdListImages(cmd *cobra.Command, args []string) error { return listImages(dataDir, output, filter) } -func cmdManifest(cmd *cobra.Command, args []string) error { +func cmdManifestWrapper(cmd *cobra.Command, args []string, w io.Writer, archChecker func(string) error) (*imagefilter.Result, error) { dataDir, err := cmd.Flags().GetString("datadir") if err != nil { - return err + return nil, err } archStr, err := cmd.Flags().GetString("arch") if err != nil { - return err + return nil, err } if archStr == "" { archStr = arch.Current().String() } distroStr, err := cmd.Flags().GetString("distro") if err != nil { - return err + return nil, err } var blueprintPath string @@ -60,30 +61,47 @@ func cmdManifest(cmd *cobra.Command, args []string) error { } bp, err := blueprintload.Load(blueprintPath) if err != nil { - return err + return nil, err } distroStr, err = findDistro(distroStr, bp.Distro) if err != nil { - return err + return nil, err } res, err := getOneImage(dataDir, distroStr, imgTypeStr, archStr) if err != nil { - return err + return nil, err } - repos, err := newRepoRegistry(dataDir) - if err != nil { - return err + if archChecker != nil { + if err := archChecker(res.Arch.Name()); err != nil { + return nil, err + } } - // XXX: add --rpmmd/cachedir option like bib - mg, err := manifestgen.New(repos, &manifestgen.Options{ - Output: osStdout, + + err = generateManifest(dataDir, blueprintPath, res, w) + return res, err +} + +func cmdManifest(cmd *cobra.Command, args []string) error { + _, err := cmdManifestWrapper(cmd, args, osStdout, nil) + return err +} + +func cmdBuild(cmd *cobra.Command, args []string) error { + var mf bytes.Buffer + + // XXX: check env here, i.e. if user is root and osbuild is installed + res, err := cmdManifestWrapper(cmd, args, &mf, func(archStr string) error { + if archStr != arch.Current().String() { + return fmt.Errorf("cannot build for arch %q from %q", archStr, arch.Current().String()) + } + return nil }) if err != nil { return err } - return mg.Generate(bp, res.Distro, res.ImgType, res.Arch, nil) + return buildImage(res, mf.Bytes()) } func run() error { @@ -128,6 +146,16 @@ operating sytsems like centos and RHEL with easy customizations support.`, manifestCmd.Flags().String("distro", "", `build manifest for a different distroname (e.g. centos-9)`) rootCmd.AddCommand(manifestCmd) + buildCmd := &cobra.Command{ + Use: "build [blueprint]", + Short: "Build the given distro/image-type, e.g. centos-9 qcow2", + RunE: cmdBuild, + SilenceUsage: true, + Args: cobra.RangeArgs(1, 2), + } + buildCmd.Flags().AddFlagSet(manifestCmd.Flags()) + rootCmd.AddCommand(buildCmd) + return rootCmd.Execute() } diff --git a/cmd/image-builder/main_test.go b/cmd/image-builder/main_test.go index a265ef4..e16e3c4 100644 --- a/cmd/image-builder/main_test.go +++ b/cmd/image-builder/main_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "os" "path/filepath" + "slices" "testing" "github.com/sirupsen/logrus" @@ -14,6 +15,7 @@ import ( "github.com/osbuild/image-builder-cli/cmd/image-builder" "github.com/osbuild/image-builder-cli/internal/manifesttest" + "github.com/osbuild/image-builder-cli/internal/testutil" ) func init() { @@ -225,3 +227,87 @@ func TestManifestIntegrationCrossArch(t *testing.T) { // XXX: provide helpers in manifesttest to extract this in a nicer way assert.Contains(t, fakeStdout.String(), `.el9.s390x.rpm`) } + +func TestBuildIntegrationHappy(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() + + restore = main.MockOsArgs([]string{ + "build", + "qcow2", + makeTestBlueprint(t, testBlueprint), + "--distro", "centos-9", + }) + defer restore() + + script := `cat - > "$0".stdin` + fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", script) + defer fakeOsbuildCmd.Restore() + + err := main.Run() + assert.NoError(t, err) + + // ensure osbuild was run exactly one + assert.Equal(t, 1, len(fakeOsbuildCmd.Calls())) + // and we passed the output dir + osbuildCall := fakeOsbuildCmd.Calls()[0] + outputDirPos := slices.Index(osbuildCall, "--output-directory") + assert.True(t, outputDirPos > -1) + assert.Equal(t, "centos-9-qcow2-x86_64", osbuildCall[outputDirPos+1]) + + // ... and that the manifest passed to osbuild + manifest, err := os.ReadFile(fakeOsbuildCmd.Path() + ".stdin") + assert.NoError(t, err) + // XXX: provide helpers in manifesttest to extract this in a nicer way + assert.Contains(t, string(manifest), `{"type":"org.osbuild.users","options":{"users":{"alice":{}}}}`) + assert.Contains(t, string(manifest), `"image":{"name":"registry.gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/fedora-minimal"`) +} + +func TestBuildIntegrationErrors(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() + + var fakeStdout, fakeStderr bytes.Buffer + restore = main.MockOsStdout(&fakeStdout) + defer restore() + restore = main.MockOsStderr(&fakeStderr) + defer restore() + + restore = main.MockOsArgs([]string{ + "build", + "qcow2", + makeTestBlueprint(t, testBlueprint), + "--distro", "centos-9", + }) + defer restore() + + script := ` +cat - > "$0".stdin +>&2 echo "error on stderr" +exit 1 +` + fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", script) + defer fakeOsbuildCmd.Restore() + + err := main.Run() + assert.EqualError(t, err, "running osbuild failed: exit status 1") + // ensure errors from osbuild are passed to the user + // XXX: once the osbuild.Status is used, also check that stdout + // is available (but that cannot be done with the existing + // osbuild-exec.go) + assert.Equal(t, "error on stderr\n", fakeStderr.String()) +} diff --git a/cmd/image-builder/manifest.go b/cmd/image-builder/manifest.go new file mode 100644 index 0000000..fb6ee2c --- /dev/null +++ b/cmd/image-builder/manifest.go @@ -0,0 +1,30 @@ +package main + +import ( + "io" + + "github.com/osbuild/images/pkg/imagefilter" + + "github.com/osbuild/image-builder-cli/internal/blueprintload" + "github.com/osbuild/image-builder-cli/internal/manifestgen" +) + +func generateManifest(dataDir, blueprintPath string, res *imagefilter.Result, output io.Writer) error { + repos, err := newRepoRegistry(dataDir) + if err != nil { + return err + } + // XXX: add --rpmmd/cachedir option like bib + mg, err := manifestgen.New(repos, &manifestgen.Options{ + Output: output, + }) + if err != nil { + return err + } + bp, err := blueprintload.Load(blueprintPath) + if err != nil { + return err + } + + return mg.Generate(bp, res.Distro, res.ImgType, res.Arch, nil) +}