Skip to content

Commit

Permalink
imagefilter: add ResultFormatter type to support flexible output
Browse files Browse the repository at this point in the history
This commit adds a new `ResultFormatter` interface that supports
outputing the results as text or JSON.

Note that the text output should be copy/paste friendly, i.e. the
"image-builder" cmdline (or other consuemrs) should support:
```
$ image-builder manifest centos-9 type:qcow2 arch:s390
$ image-builder build centos-9 type:qcow2 arch:x86_64
```
(and of course `build` will need to error there is an architecture
 mistach like host-arch != target-arch).
  • Loading branch information
mvo5 authored and achilleas-k committed Nov 12, 2024
1 parent 70fd73a commit abb3328
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 0 deletions.
96 changes: 96 additions & 0 deletions pkg/imagefilter/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package imagefilter

import (
"encoding/json"
"errors"
"fmt"
"io"
)

// OutputFormat contains the valid output formats for formatting results
type OutputFormat string

const (
OutputFormatDefault OutputFormat = ""
OutputFormatText OutputFormat = "text"
OutputFormatJSON OutputFormat = "json"
)

// ResultFormatter will format the given result list to the given io.Writer
type ResultsFormatter interface {
Output(io.Writer, []Result) error
}

// NewResultFormatter will create a formatter based on the given format.
func NewResultsFormatter(format OutputFormat) (ResultsFormatter, error) {
switch format {
case OutputFormatDefault, OutputFormatText:
return &textResultsFormatter{}, nil
case OutputFormatJSON:
return &jsonResultsFormatter{}, nil
default:
return nil, fmt.Errorf("unsupported formatter %q", format)
}
}

type textResultsFormatter struct{}

func (*textResultsFormatter) Output(w io.Writer, all []Result) error {
var errs []error

for _, res := range all {
// The should be copy/paste friendly, i.e. the "image-builder"
// cmdline should support:
// image-builder manifest centos-9 type:qcow2 arch:s390
// image-builder build centos-9 type:qcow2 arch:x86_64
if _, err := fmt.Fprintf(w, "%s type:%s arch:%s\n", res.Distro.Name(), res.ImgType.Name(), res.Arch.Name()); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}

return nil
}

type jsonResultsFormatter struct{}

type distroResultJSON struct {
Name string `json:"name"`
}

type archResultJSON struct {
Name string `json:"name"`
}

type imgTypeResultJSON struct {
Name string `json:"name"`
}

type filteredResultJSON struct {
Distro distroResultJSON `json:"distro"`
Arch archResultJSON `json:"arch"`
ImgType imgTypeResultJSON `json:"image_type"`
}

func (*jsonResultsFormatter) Output(w io.Writer, all []Result) error {
var out []filteredResultJSON

for _, res := range all {
out = append(out, filteredResultJSON{
Distro: distroResultJSON{
Name: res.Distro.Name(),
},
Arch: archResultJSON{
Name: res.Arch.Name(),
},
ImgType: imgTypeResultJSON{
Name: res.ImgType.Name(),
},
})
}

enc := json.NewEncoder(w)
return enc.Encode(out)
}
81 changes: 81 additions & 0 deletions pkg/imagefilter/formatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package imagefilter_test

import (
"bytes"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/imagefilter"
)

func newFakeResult(t *testing.T, resultSpec string) imagefilter.Result {
fac := distrofactory.NewTestDefault()

l := strings.Split(resultSpec, ":")
require.Equal(t, len(l), 3)

// XXX: it would be nice if TestDistro would support constructing
// like GetDistro("rhel-8.1:i386,amd64:ami,qcow2") that then
// creates test distro/type/arch on the fly instead of the current
// very static setup
di := fac.GetDistro(l[0])
require.NotNil(t, di)
ar, err := di.GetArch(l[2])
require.NoError(t, err)
im, err := ar.GetImageType(l[1])
require.NoError(t, err)
return imagefilter.Result{di, ar, im}
}

func TestResultsFormatter(t *testing.T) {

for _, tc := range []struct {
formatter string
fakeResults []string
expectsOutput string
}{
{
"",
[]string{"test-distro-1:qcow2:test_arch3"},
"test-distro-1 type:qcow2 arch:test_arch3\n",
},
{
"text",
[]string{"test-distro-1:qcow2:test_arch3"},
"test-distro-1 type:qcow2 arch:test_arch3\n",
},
{
"text",
[]string{
"test-distro-1:qcow2:test_arch3",
"test-distro-1:test_type:test_arch",
},
"test-distro-1 type:qcow2 arch:test_arch3\n" +
"test-distro-1 type:test_type arch:test_arch\n",
},
{
"json",
[]string{
"test-distro-1:qcow2:test_arch3",
"test-distro-1:test_type:test_arch",
},
`[{"distro":{"name":"test-distro-1"},"arch":{"name":"test_arch3"},"image_type":{"name":"qcow2"}},{"distro":{"name":"test-distro-1"},"arch":{"name":"test_arch"},"image_type":{"name":"test_type"}}]` + "\n",
},
} {
res := make([]imagefilter.Result, len(tc.fakeResults))
for i, resultSpec := range tc.fakeResults {
res[i] = newFakeResult(t, resultSpec)
}

var buf bytes.Buffer
fmter, err := imagefilter.NewResultsFormatter(imagefilter.OutputFormat(tc.formatter))
require.NoError(t, err)
err = fmter.Output(&buf, res)
assert.NoError(t, err)
assert.Equal(t, tc.expectsOutput, buf.String(), tc)
}
}

0 comments on commit abb3328

Please sign in to comment.