cmd: add new build command

This commit adds the `build` command. It takes the same flags as
`manifest` and will build the specified image.
This commit is contained in:
Michael Vogt 2024-12-03 10:17:47 +01:00 committed by Ondřej Budai
parent ce8dd45821
commit e5b3ccd6ed
5 changed files with 193 additions and 16 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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 <image-type> [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()
}

View file

@ -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())
}

View file

@ -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)
}