main: initial version of image-builder with basic --list-images
This commit adds the new `image-builder` binary. This binary is meant to build images from the CLI without the need to setup a daemon. The main use-case is CI/CD and admins running this in scripts or ad-hoc. The CLI should be pleasant to use. This first commit adds the `list-images` command which is a thin wrapper around functionality from the `osbuild/images` library. It will list all buildable images by default and can be trimmed down further via `--filter` which supports the filtering from the `images` library, see https://github.com/osbuild/images/pull/1015 It also supports `--output` which will output the result in the given format. Currently "text" and "json" are supported. Note that this will not work on it's own yet, it will need an installed image-builder to get the repositories. This will need to get fixed via either: 1. a dependency package for `ibuilder` that carries all the repos 2. a shared repo that contains the repos 3. using go:embed to get them (see images#1038)
This commit is contained in:
parent
12d60a9c74
commit
7a838332c8
8 changed files with 813 additions and 0 deletions
44
cmd/image-builder/export_test.go
Normal file
44
cmd/image-builder/export_test.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/osbuild/images/pkg/reporegistry"
|
||||
)
|
||||
|
||||
func MockOsArgs(new []string) (restore func()) {
|
||||
saved := os.Args
|
||||
os.Args = append([]string{"argv0"}, new...)
|
||||
return func() {
|
||||
os.Args = saved
|
||||
}
|
||||
}
|
||||
|
||||
func MockOsStdout(new io.Writer) (restore func()) {
|
||||
saved := osStdout
|
||||
osStdout = new
|
||||
return func() {
|
||||
osStdout = saved
|
||||
}
|
||||
}
|
||||
|
||||
func MockOsStderr(new io.Writer) (restore func()) {
|
||||
saved := osStderr
|
||||
osStderr = new
|
||||
return func() {
|
||||
osStderr = saved
|
||||
}
|
||||
}
|
||||
|
||||
func MockNewRepoRegistry(f func() (*reporegistry.RepoRegistry, error)) (restore func()) {
|
||||
saved := newRepoRegistry
|
||||
newRepoRegistry = f
|
||||
return func() {
|
||||
newRepoRegistry = saved
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
Run = run
|
||||
)
|
||||
15
cmd/image-builder/filters.go
Normal file
15
cmd/image-builder/filters.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/osbuild/images/pkg/distrofactory"
|
||||
"github.com/osbuild/images/pkg/imagefilter"
|
||||
)
|
||||
|
||||
func newImageFilterDefault() (*imagefilter.ImageFilter, error) {
|
||||
fac := distrofactory.NewDefault()
|
||||
repos, err := newRepoRegistry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return imagefilter.New(fac, repos)
|
||||
}
|
||||
29
cmd/image-builder/list.go
Normal file
29
cmd/image-builder/list.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/osbuild/images/pkg/imagefilter"
|
||||
)
|
||||
|
||||
func listImages(out io.Writer, output string, filterExprs []string) error {
|
||||
imageFilter, err := newImageFilterDefault()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filteredResult, err := imageFilter.Filter(filterExprs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmter, err := imagefilter.NewResultsFormatter(imagefilter.OutputFormat(output))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fmter.Output(out, filteredResult); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
66
cmd/image-builder/main.go
Normal file
66
cmd/image-builder/main.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
osStdout io.Writer = os.Stdout
|
||||
osStderr io.Writer = os.Stderr
|
||||
)
|
||||
|
||||
func cmdListImages(cmd *cobra.Command, args []string) error {
|
||||
filter, err := cmd.Flags().GetStringArray("filter")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output, err := cmd.Flags().GetString("output")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return listImages(osStdout, output, filter)
|
||||
}
|
||||
|
||||
func run() error {
|
||||
// images logs a bunch of stuff to Debug/Info that is distracting
|
||||
// the user (at least by default, like what repos being loaded)
|
||||
logrus.SetLevel(logrus.WarnLevel)
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "image-builder",
|
||||
Short: "Build operating system images from a given distro/image-type/blueprint",
|
||||
Long: `Build operating system images from a given distribution,
|
||||
image-type and blueprint.
|
||||
|
||||
Image-builder builds operating system images for a range of predefined
|
||||
operating sytsems like centos and RHEL with easy customizations support.`,
|
||||
SilenceErrors: true,
|
||||
}
|
||||
rootCmd.SetOut(osStdout)
|
||||
rootCmd.SetErr(osStderr)
|
||||
|
||||
listImagesCmd := &cobra.Command{
|
||||
Use: "list-images",
|
||||
Short: "List buildable images, use --filter to limit further",
|
||||
RunE: cmdListImages,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
listImagesCmd.Flags().StringArray("filter", nil, `Filter distributions by a specific criteria (e.g. "type:rhel*")`)
|
||||
listImagesCmd.Flags().String("output", "", "Output in a specific format (text, json)")
|
||||
rootCmd.AddCommand(listImagesCmd)
|
||||
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintf(osStderr, "error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
98
cmd/image-builder/main_test.go
Normal file
98
cmd/image-builder/main_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package main_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
testrepos "github.com/osbuild/images/test/data/repositories"
|
||||
|
||||
"github.com/osbuild/image-builder-cli/cmd/image-builder"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// silence logrus by default, it is quite verbose
|
||||
logrus.SetLevel(logrus.WarnLevel)
|
||||
}
|
||||
|
||||
func TestListImagesNoArguments(t *testing.T) {
|
||||
restore := main.MockNewRepoRegistry(testrepos.New)
|
||||
defer restore()
|
||||
|
||||
for _, args := range [][]string{nil, []string{"--output=text"}} {
|
||||
restore = main.MockOsArgs(append([]string{"list-images"}, args...))
|
||||
defer restore()
|
||||
|
||||
var fakeStdout bytes.Buffer
|
||||
restore = main.MockOsStdout(&fakeStdout)
|
||||
defer restore()
|
||||
|
||||
err := main.Run()
|
||||
assert.NoError(t, err)
|
||||
// we expect at least this canary
|
||||
assert.Contains(t, fakeStdout.String(), "rhel-10.0 type:qcow2 arch:x86_64\n")
|
||||
// output is sorted, i.e. 8.9 comes before 8.10
|
||||
assert.Regexp(t, `(?ms)rhel-8.9.*rhel-8.10`, fakeStdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListImagesNoArgsOutputJSON(t *testing.T) {
|
||||
restore := main.MockNewRepoRegistry(testrepos.New)
|
||||
defer restore()
|
||||
|
||||
restore = main.MockOsArgs([]string{"list-images", "--output=json"})
|
||||
defer restore()
|
||||
|
||||
var fakeStdout bytes.Buffer
|
||||
restore = main.MockOsStdout(&fakeStdout)
|
||||
defer restore()
|
||||
|
||||
err := main.Run()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// smoke test only, we expect valid json and at least the
|
||||
// distro/arch/image_type keys in the json
|
||||
var jo []map[string]interface{}
|
||||
err = json.Unmarshal(fakeStdout.Bytes(), &jo)
|
||||
assert.NoError(t, err)
|
||||
res := jo[0]
|
||||
for _, key := range []string{"distro", "arch", "image_type"} {
|
||||
assert.NotNil(t, res[key])
|
||||
}
|
||||
}
|
||||
|
||||
func TestListImagesFilteringSmoke(t *testing.T) {
|
||||
restore := main.MockNewRepoRegistry(testrepos.New)
|
||||
defer restore()
|
||||
|
||||
restore = main.MockOsArgs([]string{"list-images", "--filter=centos*"})
|
||||
defer restore()
|
||||
|
||||
var fakeStdout bytes.Buffer
|
||||
restore = main.MockOsStdout(&fakeStdout)
|
||||
defer restore()
|
||||
|
||||
err := main.Run()
|
||||
assert.NoError(t, err)
|
||||
// we have centos
|
||||
assert.Contains(t, fakeStdout.String(), "centos-9 type:qcow2 arch:x86_64\n")
|
||||
// but not rhel
|
||||
assert.NotContains(t, fakeStdout.String(), "rhel")
|
||||
}
|
||||
|
||||
func TestBadCmdErrorsNoExtraCobraNoise(t *testing.T) {
|
||||
var fakeStderr bytes.Buffer
|
||||
restore := main.MockOsStderr(&fakeStderr)
|
||||
defer restore()
|
||||
|
||||
restore = main.MockOsArgs([]string{"bad-command"})
|
||||
defer restore()
|
||||
|
||||
err := main.Run()
|
||||
assert.EqualError(t, err, `unknown command "bad-command" for "image-builder"`)
|
||||
// no extra output from cobra
|
||||
assert.Equal(t, "", fakeStderr.String())
|
||||
}
|
||||
21
cmd/image-builder/repos.go
Normal file
21
cmd/image-builder/repos.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/osbuild/images/pkg/reporegistry"
|
||||
)
|
||||
|
||||
// XXX: copied from "composer", should be exported there so
|
||||
// that we keep this in sync
|
||||
// XXX2: means we need to depend on osbuild-composer-common or a new rpm
|
||||
// that provides the relevant packages *or* we use go:embed (cf images#1038)
|
||||
var repositoryConfigs = []string{
|
||||
"/etc/osbuild-composer",
|
||||
"/usr/share/osbuild-composer",
|
||||
}
|
||||
|
||||
var newRepoRegistry = func() (*reporegistry.RepoRegistry, error) {
|
||||
// TODO: add a extraReposPaths here so that users can do
|
||||
// "ibuilder --repositories ..." to add a custom path(s)
|
||||
|
||||
return reporegistry.New(repositoryConfigs)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue