Skip to content

Commit

Permalink
main: initial version of ibuilder with basic --list-images
Browse files Browse the repository at this point in the history
This commit adds the new `ibuilder` binary (name still a bit TBD but
this is the best so far IMHO). 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 osbuild/images#1015

It also supports `--output` which will output the result in the
given format. Currently "text" and "json" are supported.
  • Loading branch information
mvo5 committed Nov 18, 2024
1 parent c718759 commit 32f14b4
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 0 deletions.
44 changes: 44 additions & 0 deletions cmd/ibuilder/export_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 15 additions & 0 deletions cmd/ibuilder/filters.go
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions cmd/ibuilder/list.go
Original file line number Diff line number Diff line change
@@ -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 changes: 66 additions & 0 deletions cmd/ibuilder/main.go
Original file line number Diff line number Diff line change
@@ -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: "ibuilder",
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", err)
os.Exit(1)
}
}
98 changes: 98 additions & 0 deletions cmd/ibuilder/main_test.go
Original file line number Diff line number Diff line change
@@ -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/ibuilder"
)

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 "ibuilder"`)
// no extra output from cobra
assert.Equal(t, "", fakeStderr.String())
}
21 changes: 21 additions & 0 deletions cmd/ibuilder/repos.go
Original file line number Diff line number Diff line change
@@ -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 (c.f.#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)
}

0 comments on commit 32f14b4

Please sign in to comment.