Add support for multiple output files in different formats (#732)

This commit is contained in:
Keith Zantow
2022-01-06 17:52:20 -05:00
committed by GitHub
parent 38c4b17847
commit 5e5312c72d
51 changed files with 642 additions and 243 deletions
+116
View File
@@ -0,0 +1,116 @@
package output
import (
"fmt"
"io"
"os"
"path"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/sbom"
"github.com/hashicorp/go-multierror"
)
// streamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
type streamWriter struct {
format format.Format
out io.Writer
close func() error
}
// Write the provided SBOM to the data stream
func (w *streamWriter) Write(s sbom.SBOM) error {
return w.format.Encode(w.out, s)
}
// Close any resources, such as open files
func (w *streamWriter) Close() error {
if w.close != nil {
return w.close()
}
return nil
}
// multiWriter holds a list of child sbom.Writers to apply all Write and Close operations to
type multiWriter struct {
writers []sbom.Writer
}
// Write writes the SBOM to all writers
func (m *multiWriter) Write(s sbom.SBOM) (errs error) {
for _, w := range m.writers {
err := w.Write(s)
if err != nil {
errs = multierror.Append(errs, err)
}
}
return errs
}
// Close closes all writers
func (m *multiWriter) Close() (errs error) {
for _, w := range m.writers {
err := w.Close()
if err != nil {
errs = multierror.Append(errs, err)
}
}
return errs
}
// WriterOption Format and path strings used to create sbom.Writer
type WriterOption struct {
Format format.Format
Path string
}
// MakeWriter create all report writers from input options; if a file is not specified, os.Stdout is used
func MakeWriter(options ...WriterOption) (_ sbom.Writer, errs error) {
if len(options) == 0 {
return nil, fmt.Errorf("no output options provided")
}
out := &multiWriter{}
defer func() {
if errs != nil {
// close any previously opened files; we can't really recover from any errors
_ = out.Close()
}
}()
for _, option := range options {
switch len(option.Path) {
case 0:
out.writers = append(out.writers, &streamWriter{
format: option.Format,
out: os.Stdout,
})
default:
// create any missing subdirectories
dir := path.Dir(option.Path)
if dir != "" {
s, err := os.Stat(dir)
if err != nil {
err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ?
if err != nil {
return nil, err
}
} else if !s.IsDir() {
return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path)
}
}
fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return nil, fmt.Errorf("unable to create report file: %w", err)
}
out.writers = append(out.writers, &streamWriter{
format: option.Format,
out: fileOut,
close: fileOut.Close,
})
}
}
return out, nil
}
+168
View File
@@ -0,0 +1,168 @@
package output
import (
"strings"
"testing"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/internal/formats/text"
"github.com/stretchr/testify/assert"
)
type writerConfig struct {
format string
file string
}
func TestOutputWriter(t *testing.T) {
tmp := t.TempDir()
testName := func(options []WriterOption, err bool) string {
var out []string
for _, opt := range options {
out = append(out, string(opt.Format.Option)+"="+opt.Path)
}
errs := ""
if err {
errs = "(err)"
}
return strings.Join(out, ", ") + errs
}
tests := []struct {
outputs []WriterOption
err bool
expected []writerConfig
}{
{
outputs: []WriterOption{},
err: true,
},
{
outputs: []WriterOption{
{
Format: table.Format(),
Path: "",
},
},
expected: []writerConfig{
{
format: "table",
},
},
},
{
outputs: []WriterOption{
{
Format: syftjson.Format(),
},
},
expected: []writerConfig{
{
format: "json",
},
},
},
{
outputs: []WriterOption{
{
Format: syftjson.Format(),
Path: "test-2.json",
},
},
expected: []writerConfig{
{
format: "json",
file: "test-2.json",
},
},
},
{
outputs: []WriterOption{
{
Format: syftjson.Format(),
Path: "test-3/1.json",
},
{
Format: spdx22json.Format(),
Path: "test-3/2.json",
},
},
expected: []writerConfig{
{
format: "json",
file: "test-3/1.json",
},
{
format: "spdx-json",
file: "test-3/2.json",
},
},
},
{
outputs: []WriterOption{
{
Format: text.Format(),
},
{
Format: spdx22json.Format(),
Path: "test-4.json",
},
},
expected: []writerConfig{
{
format: "text",
},
{
format: "spdx-json",
file: "test-4.json",
},
},
},
}
for _, test := range tests {
t.Run(testName(test.outputs, test.err), func(t *testing.T) {
outputs := test.outputs
for i := range outputs {
if outputs[i].Path != "" {
outputs[i].Path = tmp + outputs[i].Path
}
}
writer, err := MakeWriter(outputs...)
if test.err {
assert.Error(t, err)
return
} else {
assert.NoError(t, err)
}
mw := writer.(*multiWriter)
assert.Len(t, mw.writers, len(test.expected))
for i, e := range test.expected {
w := mw.writers[i].(*streamWriter)
assert.Equal(t, string(w.format.Option), e.format)
if e.file != "" {
assert.FileExists(t, tmp+e.file)
}
if e.file != "" {
assert.NotNil(t, w.out)
assert.NotNil(t, w.close)
} else {
assert.NotNil(t, w.out)
assert.Nil(t, w.close)
}
}
})
}
}