Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

imagefilter: add ResultFormatter type to support flexible output #1019

Merged
merged 1 commit into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
mvo5 marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Contributor Author

@mvo5 mvo5 Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to decide a bit how much we want to expose here, it will become an API so I started very minimal, I played with this a bit and added everything in 37c0921 just to see what it would look like. The result is something like:

{
    "distro": {
      "name": "rhel-10.0",
      "Codename": "",
      "Releasever": "10",
      "OsVersion": "10.0",
      "ModulePlatformID": "platform:el10",
      "Product": "Red Hat Enterprise Linux",
      "OSTreeRef": "rhel/10/%s/edge"
    },
    "arch": {
      "name": "x86_64"
    },
    "image_type": {
      "name": "vmdk",
      "bootmode": "hybrid",
      "filename": "disk.vmdk",
      "MIMEType": "application/x-vmdk",
      "OSTreeRef": "",
      "ISOLabel": "",
      "Size": 1,
      "PartitionType": "gpt",
      "BuildPipelines": [
        "build"
      ],
      "PayloadPipelines": [
        "os",
        "image",
        "vmdk"
      ],
      "PayloadPackageSets": [
        "blueprint"
      ],
      "Exports": [
        "vmdk"
      ]
    }
  },
  {
    "distro": {
      "name": "rhel-10.0",
      "Codename": "",
      "Releasever": "10",
      "OsVersion": "10.0",
      "ModulePlatformID": "platform:el10",
      "Product": "Red Hat Enterprise Linux",
      "OSTreeRef": "rhel/10/%s/edge"
    },
    "arch": {
      "name": "x86_64"
    },
    "image_type": {
      "name": "wsl",
      "bootmode": "none",
      "filename": "disk.tar.gz",
      "MIMEType": "application/x-tar",
      "OSTreeRef": "",
      "ISOLabel": "",
      "Size": 1,
      "PartitionType": "",
      "BuildPipelines": [
        "build"
      ],
      "PayloadPipelines": [
        "os",
        "archive"
      ],
      "PayloadPackageSets": [
        "blueprint"
      ],
      "Exports": [
        "archive"
      ]
    }
  }

...

(just to give a flavor what is there)

[edit: fixed a bug in the generation]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a few config parameters and package lists to this and we have a good way to "dump" image definitions in a portable way ^_^

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