mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 15:06:36 +01:00
Simplify the SBOM writer interface (#1892)
* remove sbom.writer bytes call and consolidate helpers to options pkg Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * dont close stdout Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * remove close operation from multiwriter Signed-off-by: Alex Goodman <alex.goodman@anchore.com> --------- Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
+3
-3
@@ -56,13 +56,13 @@ func CatalogPackages(src *source.Source, cfg cataloger.Config) (*pkg.Collection,
|
||||
// otherwise conditionally use the correct set of loggers based on the input type (container image or directory)
|
||||
switch src.Metadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
log.Info("cataloging image")
|
||||
log.Info("cataloging an image")
|
||||
catalogers = cataloger.ImageCatalogers(cfg)
|
||||
case source.FileScheme:
|
||||
log.Info("cataloging file")
|
||||
log.Info("cataloging a file")
|
||||
catalogers = cataloger.AllCatalogers(cfg)
|
||||
case source.DirectoryScheme:
|
||||
log.Info("cataloging directory")
|
||||
log.Info("cataloging a directory")
|
||||
catalogers = cataloger.DirectoryCatalogers(cfg)
|
||||
default:
|
||||
return nil, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", src.Metadata.Scheme)
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
package sbom
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// multiWriter holds a list of child sbom.Writers to apply all Write and Close operations to
|
||||
type multiWriter struct {
|
||||
writers []Writer
|
||||
}
|
||||
|
||||
// WriterOption Format and path strings used to create sbom.Writer
|
||||
type WriterOption struct {
|
||||
Format Format
|
||||
Path string
|
||||
}
|
||||
|
||||
func NewWriterOption(f Format, p string) WriterOption {
|
||||
expandedPath, err := homedir.Expand(p)
|
||||
if err != nil {
|
||||
log.Warnf("could not expand given writer output path=%q: %w", p, err)
|
||||
// ignore errors
|
||||
expandedPath = p
|
||||
}
|
||||
return WriterOption{
|
||||
Format: f,
|
||||
Path: expandedPath,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWriter create all report writers from input options; if a file is not specified, os.Stdout is used
|
||||
func NewWriter(options ...WriterOption) (_ Writer, err error) {
|
||||
if len(options) == 0 {
|
||||
return nil, fmt.Errorf("no output options provided")
|
||||
}
|
||||
|
||||
out := &multiWriter{}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// close any previously opened files; we can't really recover from any errors
|
||||
if err := out.Close(); err != nil {
|
||||
log.Warnf("unable to close sbom writers: %+v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Write writes the SBOM to all writers
|
||||
func (m *multiWriter) Write(s SBOM) (errs error) {
|
||||
for _, w := range m.writers {
|
||||
err := w.Write(s)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Bytes returns the bytes of the SBOM that would be written
|
||||
func (m *multiWriter) Bytes(s SBOM) (bytes []byte, err error) {
|
||||
for _, w := range m.writers {
|
||||
b, err := w.Bytes(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes = append(bytes, b...)
|
||||
}
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
package sbom
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func dummyEncoder(io.Writer, SBOM) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func dummyFormat(name string) Format {
|
||||
return NewFormat(AnyVersion, dummyEncoder, nil, nil, FormatID(name))
|
||||
}
|
||||
|
||||
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.ID())+"="+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: dummyFormat("table"),
|
||||
Path: "",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "table",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []WriterOption{
|
||||
{
|
||||
Format: dummyFormat("json"),
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []WriterOption{
|
||||
{
|
||||
Format: dummyFormat("json"),
|
||||
Path: "test-2.json",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "json",
|
||||
file: "test-2.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []WriterOption{
|
||||
{
|
||||
Format: dummyFormat("json"),
|
||||
Path: "test-3/1.json",
|
||||
},
|
||||
{
|
||||
Format: dummyFormat("spdx-json"),
|
||||
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: dummyFormat("text"),
|
||||
},
|
||||
{
|
||||
Format: dummyFormat("spdx-json"),
|
||||
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 := NewWriter(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.ID()), 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWriterOption(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "expand home dir",
|
||||
path: "~/place.txt",
|
||||
expected: filepath.Join(homedir.Get(), "place.txt"),
|
||||
},
|
||||
{
|
||||
name: "passthrough other paths",
|
||||
path: "/other/place.txt",
|
||||
expected: "/other/place.txt",
|
||||
},
|
||||
{
|
||||
name: "no path",
|
||||
path: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := NewWriterOption(dummyFormat("table"), tt.path)
|
||||
assert.Equal(t, tt.expected, o.Path)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package sbom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// streamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
|
||||
type streamWriter struct {
|
||||
format Format
|
||||
out io.Writer
|
||||
close func() error
|
||||
}
|
||||
|
||||
// Write the provided SBOM to the data stream
|
||||
func (w *streamWriter) Write(s SBOM) error {
|
||||
return w.format.Encode(w.out, s)
|
||||
}
|
||||
|
||||
// Bytes returns the bytes of the SBOM that would be written
|
||||
func (w *streamWriter) Bytes(s SBOM) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
err := w.format.Encode(&buffer, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// Close any resources, such as open files
|
||||
func (w *streamWriter) Close() error {
|
||||
if w.close != nil {
|
||||
return w.close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+1
-11
@@ -1,16 +1,6 @@
|
||||
package sbom
|
||||
|
||||
import "io"
|
||||
|
||||
// Writer an interface to write SBOMs
|
||||
// Writer an interface to write SBOMs to a destination
|
||||
type Writer interface {
|
||||
// Write writes the provided SBOM
|
||||
Write(SBOM) error
|
||||
|
||||
// Bytes returns the bytes of the SBOM that would be written
|
||||
Bytes(SBOM) ([]byte, error)
|
||||
|
||||
// Closer a resource cleanup hook which will be called after SBOM
|
||||
// is written or if an error occurs before Write is called
|
||||
io.Closer
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user