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.

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)
  • Loading branch information
mvo5 committed Nov 18, 2024
1 parent 12d60a9 commit 417535f
Show file tree
Hide file tree
Showing 9 changed files with 812 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: 'stable'

- name: Build
run: go build -v ./...
Expand Down
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 (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

0 comments on commit 417535f

Please sign in to comment.