Internalize majority of cmd package (#2533)

* internalize majority of cmd package and migrate integration tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add internal api encoder

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* create internal representation of all formats

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* export capability to get default encoders

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* restore test fixtures

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2024-01-24 13:29:51 -05:00 committed by GitHub
parent bf3cd9ed3b
commit e0e1c4ba0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
232 changed files with 809 additions and 661 deletions

View file

@ -86,8 +86,8 @@ jobs:
- name: Restore integration test cache
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
with:
path: ${{ github.workspace }}/test/integration/test-fixtures/cache
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
path: ${{ github.workspace }}/cmd/syft/internal/test/integration/test-fixtures/cache
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('/cmd/syft/internal/test/integration/test-fixtures/cache.fingerprint') }}
- name: Run integration tests
run: make integration

1
.gitignore vendored
View file

@ -32,7 +32,6 @@ VERSION
/test/results
coverage.txt
*.log
test/integration/test-fixtures/**/go.sum
# probable archives
.images

View file

@ -234,7 +234,7 @@ rough outline how that works:
the `/test` directory. These tests should focus on correctness of functionality in depth. % test coverage metrics
only considers unit tests and no other forms of testing.
- `integration`: located within `test/integration`, these tests focus on the behavior surfaced by the common library
- `integration`: located within `cmd/syft/internal/test/integration`, these tests focus on the behavior surfaced by the common library
entrypoints from the `syft` package and make light assertions about the results surfaced. Additionally, these tests
tend to make diversity assertions for enum-like objects, ensuring that as enum values are added to a definition
that integration tests will automatically fail if no test attempts to use that enum value. For more details see

View file

@ -185,7 +185,7 @@ tasks:
- fixtures
vars:
TEST_PKGS:
sh: "go list ./... | grep -v {{ .OWNER }}/{{ .PROJECT }}/test | tr '\n' ' '"
sh: "go list ./... | grep -v {{ .OWNER }}/{{ .PROJECT }}/test | grep -v {{ .OWNER }}/{{ .PROJECT }}/cmd/syft/internal/test | tr '\n' ' '"
# unit test coverage threshold (in % coverage)
COVERAGE_THRESHOLD: 62
@ -197,7 +197,7 @@ tasks:
integration:
desc: Run integration tests
cmds:
- "go test -v ./test/integration"
- "go test -v ./cmd/syft/internal/test/integration"
# exercise most of the CLI with the data race detector
- "go run -race cmd/syft/main.go alpine:latest"
@ -265,7 +265,7 @@ tasks:
fingerprints:
desc: Generate test fixture fingerprints
generates:
- test/integration/test-fixtures/cache.fingerprint
- cmd/syft/internal/test/integration/test-fixtures/cache.fingerprint
- syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint
- syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint
- syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint
@ -275,7 +275,7 @@ tasks:
- test/cli/test-fixtures/cache.fingerprint
cmds:
# for IMAGE integration test fixtures
- "cd test/integration/test-fixtures && make cache.fingerprint"
- "cd cmd/syft/internal/test/integration/test-fixtures && make cache.fingerprint"
# for BINARY test fixtures
- "cd syft/pkg/cataloger/binary/test-fixtures && make cache.fingerprint"
# for JAVA BUILD test fixtures
@ -286,7 +286,7 @@ tasks:
- "cd syft/pkg/cataloger/redhat/test-fixtures && make rpms.fingerprint"
# for Kernel test fixtures
- "cd syft/pkg/cataloger/kernel/test-fixtures && make cache.fingerprint"
# for INSTALL integration test fixtures
# for INSTALL test fixtures
- "cd test/install && make cache.fingerprint"
# for CLI test fixtures
- "cd test/cli/test-fixtures && make cache.fingerprint"

View file

@ -9,8 +9,8 @@ import (
"github.com/anchore/clio"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/cli/commands"
handler "github.com/anchore/syft/cmd/syft/cli/ui"
"github.com/anchore/syft/cmd/syft/internal/commands"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"

View file

@ -1,129 +0,0 @@
package options
import (
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/anchore/clio"
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/github"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/format/table"
"github.com/anchore/syft/syft/format/template"
"github.com/anchore/syft/syft/format/text"
"github.com/anchore/syft/syft/sbom"
)
var _ clio.PostLoader = (*Format)(nil)
// Format contains all user configuration for output formatting.
type Format struct {
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
Template FormatTemplate `yaml:"template" json:"template" mapstructure:"template"`
SyftJSON FormatSyftJSON `yaml:"json" json:"json" mapstructure:"json"`
SPDXJSON FormatSPDXJSON `yaml:"spdx-json" json:"spdx-json" mapstructure:"spdx-json"`
CyclonedxJSON FormatCyclonedxJSON `yaml:"cyclonedx-json" json:"cyclonedx-json" mapstructure:"cyclonedx-json"`
CyclonedxXML FormatCyclonedxXML `yaml:"cyclonedx-xml" json:"cyclonedx-xml" mapstructure:"cyclonedx-xml"`
}
func (o *Format) PostLoad() error {
o.SyftJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.SyftJSON.Pretty)
o.SPDXJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.SPDXJSON.Pretty)
o.CyclonedxJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.CyclonedxJSON.Pretty)
o.CyclonedxXML.Pretty = multiLevelOption[bool](false, o.Pretty, o.CyclonedxXML.Pretty)
return nil
}
func DefaultFormat() Format {
return Format{
Template: DefaultFormatTemplate(),
SyftJSON: DefaultFormatJSON(),
SPDXJSON: DefaultFormatSPDXJSON(),
CyclonedxJSON: DefaultFormatCyclonedxJSON(),
CyclonedxXML: DefaultFormatCyclonedxXML(),
}
}
func (o *Format) Encoders() ([]sbom.FormatEncoder, error) {
// setup all encoders based on the configuration
var list encoderList
// in the future there will be application configuration options that can be used to set the default output format
list.addWithErr(template.ID)(o.Template.formatEncoders())
list.addWithErr(syftjson.ID)(o.SyftJSON.formatEncoders())
list.add(table.ID)(table.NewFormatEncoder())
list.add(text.ID)(text.NewFormatEncoder())
list.add(github.ID)(github.NewFormatEncoder())
list.addWithErr(cyclonedxxml.ID)(o.CyclonedxXML.formatEncoders())
list.addWithErr(cyclonedxjson.ID)(o.CyclonedxJSON.formatEncoders())
list.addWithErr(spdxjson.ID)(o.SPDXJSON.formatEncoders())
list.addWithErr(spdxtagvalue.ID)(spdxTagValueEncoders())
return list.encoders, list.err
}
// TODO: when application configuration is made for this format then this should be ported to the options object
// that is created for that configuration (as done with the template output option)
func spdxTagValueEncoders() ([]sbom.FormatEncoder, error) {
var (
encs []sbom.FormatEncoder
errs error
)
for _, v := range spdxtagvalue.SupportedVersions() {
enc, err := spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.EncoderConfig{Version: v})
if err != nil {
errs = multierror.Append(errs, err)
} else {
encs = append(encs, enc)
}
}
return encs, errs
}
type encoderList struct {
encoders []sbom.FormatEncoder
err error
}
func (l *encoderList) addWithErr(name sbom.FormatID) func([]sbom.FormatEncoder, error) {
return func(encs []sbom.FormatEncoder, err error) {
if err != nil {
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: %w", name, err))
return
}
for _, enc := range encs {
if enc == nil {
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: nil encoder returned", name))
continue
}
l.encoders = append(l.encoders, enc)
}
}
}
func (l *encoderList) add(name sbom.FormatID) func(...sbom.FormatEncoder) {
return func(encs ...sbom.FormatEncoder) {
for _, enc := range encs {
if enc == nil {
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: nil encoder returned", name))
continue
}
l.encoders = append(l.encoders, enc)
}
}
}
func multiLevelOption[T any](defaultValue T, option ...*T) *T {
result := defaultValue
for _, opt := range option {
if opt != nil {
result = *opt
}
}
return &result
}

View file

@ -1,43 +0,0 @@
package options
import (
"github.com/hashicorp/go-multierror"
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/sbom"
)
type FormatCyclonedxJSON struct {
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
}
func DefaultFormatCyclonedxJSON() FormatCyclonedxJSON {
return FormatCyclonedxJSON{}
}
func (o FormatCyclonedxJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
var (
encs []sbom.FormatEncoder
errs error
)
for _, v := range cyclonedxjson.SupportedVersions() {
enc, err := cyclonedxjson.NewFormatEncoderWithConfig(o.buildConfig(v))
if err != nil {
errs = multierror.Append(errs, err)
} else {
encs = append(encs, enc)
}
}
return encs, errs
}
func (o FormatCyclonedxJSON) buildConfig(version string) cyclonedxjson.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty
}
return cyclonedxjson.EncoderConfig{
Version: version,
Pretty: pretty,
}
}

View file

@ -1,43 +0,0 @@
package options
import (
"github.com/hashicorp/go-multierror"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/sbom"
)
type FormatCyclonedxXML struct {
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
}
func DefaultFormatCyclonedxXML() FormatCyclonedxXML {
return FormatCyclonedxXML{}
}
func (o FormatCyclonedxXML) formatEncoders() ([]sbom.FormatEncoder, error) {
var (
encs []sbom.FormatEncoder
errs error
)
for _, v := range cyclonedxxml.SupportedVersions() {
enc, err := cyclonedxxml.NewFormatEncoderWithConfig(o.buildConfig(v))
if err != nil {
errs = multierror.Append(errs, err)
} else {
encs = append(encs, enc)
}
}
return encs, errs
}
func (o FormatCyclonedxXML) buildConfig(version string) cyclonedxxml.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty
}
return cyclonedxxml.EncoderConfig{
Version: version,
Pretty: pretty,
}
}

View file

@ -1,43 +0,0 @@
package options
import (
"github.com/hashicorp/go-multierror"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/sbom"
)
type FormatSPDXJSON struct {
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
}
func DefaultFormatSPDXJSON() FormatSPDXJSON {
return FormatSPDXJSON{}
}
func (o FormatSPDXJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
var (
encs []sbom.FormatEncoder
errs error
)
for _, v := range spdxjson.SupportedVersions() {
enc, err := spdxjson.NewFormatEncoderWithConfig(o.buildConfig(v))
if err != nil {
errs = multierror.Append(errs, err)
} else {
encs = append(encs, enc)
}
}
return encs, errs
}
func (o FormatSPDXJSON) buildConfig(v string) spdxjson.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty
}
return spdxjson.EncoderConfig{
Version: v,
Pretty: pretty,
}
}

View file

@ -13,7 +13,7 @@ import (
"github.com/wagoodman/go-progress"
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"

View file

@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)

View file

@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra"
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"

View file

@ -14,7 +14,7 @@ import (
"github.com/anchore/clio"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"

View file

@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/options"
)
func Test_scanOptions_validateLegacyOptionsNotUsed(t *testing.T) {

View file

@ -11,8 +11,8 @@ import (
"github.com/anchore/clio"
hashiVersion "github.com/anchore/go-version"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event"

View file

@ -0,0 +1,58 @@
package options
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/sbom"
)
var _ clio.PostLoader = (*Format)(nil)
// Format contains all user configuration for output formatting.
type Format struct {
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
Template FormatTemplate `yaml:"template" json:"template" mapstructure:"template"`
SyftJSON FormatSyftJSON `yaml:"json" json:"json" mapstructure:"json"`
SPDXJSON FormatSPDXJSON `yaml:"spdx-json" json:"spdx-json" mapstructure:"spdx-json"`
CyclonedxJSON FormatCyclonedxJSON `yaml:"cyclonedx-json" json:"cyclonedx-json" mapstructure:"cyclonedx-json"`
CyclonedxXML FormatCyclonedxXML `yaml:"cyclonedx-xml" json:"cyclonedx-xml" mapstructure:"cyclonedx-xml"`
}
func (o *Format) PostLoad() error {
o.SyftJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.SyftJSON.Pretty)
o.SPDXJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.SPDXJSON.Pretty)
o.CyclonedxJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.CyclonedxJSON.Pretty)
o.CyclonedxXML.Pretty = multiLevelOption[bool](false, o.Pretty, o.CyclonedxXML.Pretty)
return nil
}
func DefaultFormat() Format {
return Format{
Template: DefaultFormatTemplate(),
SyftJSON: DefaultFormatJSON(),
SPDXJSON: DefaultFormatSPDXJSON(),
CyclonedxJSON: DefaultFormatCyclonedxJSON(),
CyclonedxXML: DefaultFormatCyclonedxXML(),
}
}
func (o Format) Encoders() ([]sbom.FormatEncoder, error) {
return format.EncodersConfig{
Template: o.Template.config(),
SyftJSON: o.SyftJSON.config(),
SPDXJSON: o.SPDXJSON.config(format.AllVersions), // we support multiple versions, not just a single version
CyclonedxJSON: o.CyclonedxJSON.config(format.AllVersions), // we support multiple versions, not just a single version
CyclonedxXML: o.CyclonedxXML.config(format.AllVersions), // we support multiple versions, not just a single version
}.Encoders()
}
func multiLevelOption[T any](defaultValue T, option ...*T) *T {
result := defaultValue
for _, opt := range option {
if opt != nil {
result = *opt
}
}
return &result
}

View file

@ -0,0 +1,24 @@
package options
import (
"github.com/anchore/syft/syft/format/cyclonedxjson"
)
type FormatCyclonedxJSON struct {
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
}
func DefaultFormatCyclonedxJSON() FormatCyclonedxJSON {
return FormatCyclonedxJSON{}
}
func (o FormatCyclonedxJSON) config(version string) cyclonedxjson.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty
}
return cyclonedxjson.EncoderConfig{
Version: version,
Pretty: pretty,
}
}

View file

@ -13,7 +13,7 @@ func TestFormatCyclonedxJSON_buildConfig(t *testing.T) {
ft := &FormatCyclonedxJSON{}
ft = setAllToNonZero(t, ft).(*FormatCyclonedxJSON)
subject := ft.buildConfig("Version")
subject := ft.config("Version")
assertExpectedValue(t, subject)
}

View file

@ -0,0 +1,24 @@
package options
import (
"github.com/anchore/syft/syft/format/cyclonedxxml"
)
type FormatCyclonedxXML struct {
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
}
func DefaultFormatCyclonedxXML() FormatCyclonedxXML {
return FormatCyclonedxXML{}
}
func (o FormatCyclonedxXML) config(version string) cyclonedxxml.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty
}
return cyclonedxxml.EncoderConfig{
Version: version,
Pretty: pretty,
}
}

View file

@ -10,6 +10,6 @@ func TestFormatCyclonedxXML_buildConfig(t *testing.T) {
ft := FormatCyclonedxXML{}
ftp := setAllToNonZero(t, &ft).(*FormatCyclonedxXML)
subject := ftp.buildConfig("Version")
subject := ftp.config("Version")
assertExpectedValue(t, subject)
}

View file

@ -0,0 +1,24 @@
package options
import (
"github.com/anchore/syft/syft/format/spdxjson"
)
type FormatSPDXJSON struct {
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
}
func DefaultFormatSPDXJSON() FormatSPDXJSON {
return FormatSPDXJSON{}
}
func (o FormatSPDXJSON) config(v string) spdxjson.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty
}
return spdxjson.EncoderConfig{
Version: v,
Pretty: pretty,
}
}

View file

@ -10,6 +10,6 @@ func TestFormatSPDXJSON_buildConfig(t *testing.T) {
ft := &FormatSPDXJSON{}
ft = setAllToNonZero(t, ft).(*FormatSPDXJSON)
subject := ft.buildConfig("Version")
subject := ft.config("Version")
assertExpectedValue(t, subject)
}

View file

@ -2,7 +2,6 @@ package options
import (
"github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/sbom"
)
type FormatSyftJSON struct {
@ -16,12 +15,7 @@ func DefaultFormatJSON() FormatSyftJSON {
}
}
func (o FormatSyftJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
enc, err := syftjson.NewFormatEncoderWithConfig(o.buildConfig())
return []sbom.FormatEncoder{enc}, err
}
func (o FormatSyftJSON) buildConfig() syftjson.EncoderConfig {
func (o FormatSyftJSON) config() syftjson.EncoderConfig {
var pretty bool
if o.Pretty != nil {
pretty = *o.Pretty

View file

@ -10,6 +10,6 @@ func TestFormatSyftJSON_buildConfig(t *testing.T) {
ft := &FormatSyftJSON{}
ft = setAllToNonZero(t, ft).(*FormatSyftJSON)
subject := ft.buildConfig()
subject := ft.config()
assertExpectedValue(t, subject)
}

View file

@ -3,7 +3,6 @@ package options
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/format/template"
"github.com/anchore/syft/syft/sbom"
)
var _ clio.FlagAdder = (*FormatTemplate)(nil)
@ -26,12 +25,8 @@ func (o *FormatTemplate) AddFlags(flags clio.FlagSet) {
}
}
func (o FormatTemplate) formatEncoders() ([]sbom.FormatEncoder, error) {
if !o.Enabled {
return nil, nil
}
enc, err := template.NewFormatEncoder(template.EncoderConfig{
func (o FormatTemplate) config() template.EncoderConfig {
return template.EncoderConfig{
TemplatePath: o.Path,
})
return []sbom.FormatEncoder{enc}, err
}
}

View file

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"

View file

@ -12,7 +12,7 @@ import (
"github.com/anchore/clio"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/syft/cataloging/filecataloging"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/file/cataloger/filecontent"

View file

@ -5,3 +5,6 @@
# functionality), committing it seems like an acceptable exception.
!image-pkg-coverage/pkgs/java/*.jar
!image-pkg-coverage/pkgs/java/*.hpi
**/go.sum
!image-go-bin-arch-coverage/go.sum

Some files were not shown because too many files have changed in this diff Show more