Add platform selection (#866)

This commit is contained in:
Alex Goodman 2022-03-04 17:41:38 -05:00 committed by GitHub
parent 4af32c5bee
commit a86dd3704e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 99 additions and 40 deletions

View File

@ -131,7 +131,7 @@ func selectPassFunc(keypath string) (cosign.PassFunc, error) {
func attestExec(ctx context.Context, _ *cobra.Command, args []string) error { func attestExec(ctx context.Context, _ *cobra.Command, args []string) error {
// can only be an image for attestation or OCI DIR // can only be an image for attestation or OCI DIR
userInput := args[0] userInput := args[0]
si, err := source.ParseInput(userInput, false) si, err := source.ParseInput(userInput, appConfig.Platform, false)
if err != nil { if err != nil {
return fmt.Errorf("could not generate source input for attest command: %w", err) return fmt.Errorf("could not generate source input for attest command: %w", err)
} }
@ -304,6 +304,11 @@ func setAttestFlags(flags *pflag.FlagSet) {
"output", "o", formatAliases(syftjson.ID)[0], "output", "o", formatAliases(syftjson.ID)[0],
fmt.Sprintf("the SBOM format encapsulated within the attestation, available options=%v", formatAliases(attestFormats...)), fmt.Sprintf("the SBOM format encapsulated within the attestation, available options=%v", formatAliases(attestFormats...)),
) )
flags.StringP(
"platform", "", "",
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')",
)
} }
func bindAttestConfigOptions(flags *pflag.FlagSet) error { func bindAttestConfigOptions(flags *pflag.FlagSet) error {

View File

@ -72,8 +72,8 @@ func initCmdAliasBindings() {
panic(err) panic(err)
} }
case attestCmd: case attestCmd:
// the --output option is an independently defined flag, but a shared config option // the --output and --platform options are independently defined flags, but a shared config option
if err = bindSharedOutputConfigOption(attestCmd.Flags()); err != nil { if err = bindSharedConfigOption(attestCmd.Flags()); err != nil {
panic(err) panic(err)
} }
// even though the root command or packages command is NOT being run, we still need default bindings // even though the root command or packages command is NOT being run, we still need default bindings
@ -90,11 +90,15 @@ func initCmdAliasBindings() {
} }
} }
func bindSharedOutputConfigOption(flags *pflag.FlagSet) error { func bindSharedConfigOption(flags *pflag.FlagSet) error {
if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil { if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil {
return err return err
} }
if err := viper.BindPFlag("platform", flags.Lookup("platform")); err != nil {
return err
}
return nil return nil
} }

View File

@ -6,17 +6,15 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/syft"
"github.com/anchore/stereoscope" "github.com/anchore/stereoscope"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/anchore" "github.com/anchore/syft/internal/anchore"
"github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui" "github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version" "github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg/cataloger" "github.com/anchore/syft/syft/pkg/cataloger"
@ -112,6 +110,11 @@ func setPackageFlags(flags *pflag.FlagSet) {
"file to write the default report output to (default is STDOUT)", "file to write the default report output to (default is STDOUT)",
) )
flags.StringP(
"platform", "", "",
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')",
)
// Upload options ////////////////////////////////////////////////////////// // Upload options //////////////////////////////////////////////////////////
flags.StringP( flags.StringP(
"host", "H", "", "host", "H", "",
@ -153,7 +156,7 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
if err := bindExclusivePackagesConfigOptions(flags); err != nil { if err := bindExclusivePackagesConfigOptions(flags); err != nil {
return err return err
} }
if err := bindSharedOutputConfigOption(flags); err != nil { if err := bindSharedConfigOption(flags); err != nil {
return err return err
} }
return nil return nil
@ -232,7 +235,7 @@ func packagesExec(_ *cobra.Command, args []string) error {
// could be an image or a directory, with or without a scheme // could be an image or a directory, with or without a scheme
userInput := args[0] userInput := args[0]
si, err := source.ParseInput(userInput, true) si, err := source.ParseInput(userInput, appConfig.Platform, true)
if err != nil { if err != nil {
return fmt.Errorf("could not generate source input for packages command: %w", err) return fmt.Errorf("could not generate source input for packages command: %w", err)
} }

View File

@ -114,7 +114,7 @@ func powerUserExecWorker(userInput string, writer sbom.Writer) <-chan error {
return return
} }
si, err := source.ParseInput(userInput, true) si, err := source.ParseInput(userInput, appConfig.Platform, true)
if err != nil { if err != nil {
errs <- err errs <- err
return return

2
go.mod
View File

@ -13,7 +13,7 @@ require (
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
github.com/anchore/stereoscope v0.0.0-20220301220648-8aa8a4a0bf50 github.com/anchore/stereoscope v0.0.0-20220304014943-22a4c2bb498e
github.com/antihax/optional v1.0.0 github.com/antihax/optional v1.0.0
github.com/bmatcuk/doublestar/v4 v4.0.2 github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/docker/docker v20.10.12+incompatible github.com/docker/docker v20.10.12+incompatible

4
go.sum
View File

@ -284,8 +284,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk= github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk=
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0= github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0=
github.com/anchore/stereoscope v0.0.0-20220301220648-8aa8a4a0bf50 h1:+Fe67xv6NRLdZ5V9X2kw959XsKQCUx5/6RL/wQZfs44= github.com/anchore/stereoscope v0.0.0-20220304014943-22a4c2bb498e h1:+nlnKqlR8UwG/PhTD8qSN7RphXOI9fK77q5p3PQLx0k=
github.com/anchore/stereoscope v0.0.0-20220301220648-8aa8a4a0bf50/go.mod h1:QpDHHV2h1NNfu7klzU75XC8RvSlaPK6HHgi0dy8A6sk= github.com/anchore/stereoscope v0.0.0-20220304014943-22a4c2bb498e/go.mod h1:Juw0DqHmBSAMFVcT/kRM0GhLdIEAaiCQbq9mdCp47dY=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=

View File

@ -46,6 +46,7 @@ type Application struct {
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"` Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"` Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
} }
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command // PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command

View File

@ -163,7 +163,9 @@
], ],
"manifest": "ZXlKelkyaGxiV0ZXWlhKemFXOXVJam95TENKdFpXUnBZVlI1Y0dVaU9pSmguLi4=", "manifest": "ZXlKelkyaGxiV0ZXWlhKemFXOXVJam95TENKdFpXUnBZVlI1Y0dVaU9pSmguLi4=",
"config": "ZXlKaGNtTm9hWFJsWTNSMWNtVWlPaUpoYldRMk5DSXNJbU52Ym1acC4uLg==", "config": "ZXlKaGNtTm9hWFJsWTNSMWNtVWlPaUpoYldRMk5DSXNJbU52Ym1acC4uLg==",
"repoDigests": [] "repoDigests": [],
"architecture": "",
"os": ""
} }
}, },
"distro": { "distro": {

View File

@ -1,7 +1,7 @@
{ {
"artifacts": [ "artifacts": [
{ {
"id": "3a6c7061f86ac3f7", "id": "d9527e708c11f8b9",
"name": "package-1", "name": "package-1",
"version": "1.0.1", "version": "1.0.1",
"type": "python", "type": "python",
@ -9,7 +9,7 @@
"locations": [ "locations": [
{ {
"path": "/somefile-1.txt", "path": "/somefile-1.txt",
"layerID": "sha256:135c16aca35f76c25a18cb6650c621a46b8b79864ad6f2834167de2679bb587d" "layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59"
} }
], ],
"licenses": [ "licenses": [
@ -32,7 +32,7 @@
} }
}, },
{ {
"id": "4e916af2d387ce9", "id": "73f796c846875b9e",
"name": "package-2", "name": "package-2",
"version": "2.0.1", "version": "2.0.1",
"type": "deb", "type": "deb",
@ -40,7 +40,7 @@
"locations": [ "locations": [
{ {
"path": "/somefile-2.txt", "path": "/somefile-2.txt",
"layerID": "sha256:c751a2f31455b3049bcab3e3af5861c9431116c9f4a46213e44dbeff8ab36985" "layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec"
} }
], ],
"licenses": [], "licenses": [],
@ -67,7 +67,7 @@
"type": "image", "type": "image",
"target": { "target": {
"userInput": "user-image-input", "userInput": "user-image-input",
"imageID": "sha256:9998e833a66442934ad948e5cbe898630773c369ef16517623254fa46edd171b", "imageID": "sha256:2480160b55bec40c44d3b145c7b2c1c47160db8575c3dcae086d76b9370ae7ca",
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", "manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json", "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [ "tags": [
@ -77,18 +77,20 @@
"layers": [ "layers": [
{ {
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:135c16aca35f76c25a18cb6650c621a46b8b79864ad6f2834167de2679bb587d", "digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59",
"size": 22 "size": 22
}, },
{ {
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:c751a2f31455b3049bcab3e3af5861c9431116c9f4a46213e44dbeff8ab36985", "digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec",
"size": 16 "size": 16
} }
], ],
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1Njo5OTk4ZTgzM2E2NjQ0MjkzNGFkOTQ4ZTVjYmU4OTg2MzA3NzNjMzY5ZWYxNjUxNzYyMzI1NGZhNDZlZGQxNzFiIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjoxMzVjMTZhY2EzNWY3NmMyNWExOGNiNjY1MGM2MjFhNDZiOGI3OTg2NGFkNmYyODM0MTY3ZGUyNjc5YmI1ODdkIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmM3NTFhMmYzMTQ1NWIzMDQ5YmNhYjNlM2FmNTg2MWM5NDMxMTE2YzlmNGE0NjIxM2U0NGRiZWZmOGFiMzY5ODUifV19", "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjoyNDgwMTYwYjU1YmVjNDBjNDRkM2IxNDVjN2IyYzFjNDcxNjBkYjg1NzVjM2RjYWUwODZkNzZiOTM3MGFlN2NhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjItMDEtMThUMjA6MzA6MTMuMDQ0Njg1NTg5WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIyLTAxLTE4VDIwOjMwOjEyLjE5OTA3MjQxOVoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjItMDEtMThUMjA6MzA6MTMuMDQ0Njg1NTg5WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6MTM1YzE2YWNhMzVmNzZjMjVhMThjYjY2NTBjNjIxYTQ2YjhiNzk4NjRhZDZmMjgzNDE2N2RlMjY3OWJiNTg3ZCIsInNoYTI1NjpjNzUxYTJmMzE0NTViMzA0OWJjYWIzZTNhZjU4NjFjOTQzMTExNmM5ZjRhNDYyMTNlNDRkYmVmZjhhYjM2OTg1Il19fQ==", "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMDRUMTE6NDA6MDAuNjM4Mzk0NVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC41OTA3MzE2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC42MzgzOTQ1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmI2YmVlY2I3NWIzOWY0YmI4MTNkYmYxNzdlNTAxZWRkNWRkYjNlNjliYjQ1Y2VkZWI3OGM2NzZlZTFiN2E1OSIsInNoYTI1NjozMTliNTg4Y2U2NDI1M2E4N2I1MzNjOGVkMDFjZjAwMjVlMGVhYzk4ZTdiNTE2ZTEyNTMyOTU3ZTEyNDRmZGVjIl19fQ==",
"repoDigests": [] "repoDigests": [],
"architecture": "",
"os": ""
} }
}, },
"distro": { "distro": {

View File

@ -15,6 +15,9 @@ type ImageMetadata struct {
RawManifest []byte `json:"manifest"` RawManifest []byte `json:"manifest"`
RawConfig []byte `json:"config"` RawConfig []byte `json:"config"`
RepoDigests []string `json:"repoDigests"` RepoDigests []string `json:"repoDigests"`
Architecture string `json:"architecture"`
Variant string `json:"architectureVariant,omitempty"`
OS string `json:"os"`
} }
// LayerMetadata represents all static metadata that defines what a container image layer is. // LayerMetadata represents all static metadata that defines what a container image layer is.
@ -42,6 +45,9 @@ func NewImageMetadata(img *image.Image, userInput string) ImageMetadata {
RawConfig: img.Metadata.RawConfig, RawConfig: img.Metadata.RawConfig,
RawManifest: img.Metadata.RawManifest, RawManifest: img.Metadata.RawManifest,
RepoDigests: img.Metadata.RepoDigests, RepoDigests: img.Metadata.RepoDigests,
Architecture: img.Metadata.Architecture,
Variant: img.Metadata.Variant,
OS: img.Metadata.OS,
} }
// populate image metadata // populate image metadata

View File

@ -40,12 +40,13 @@ type Input struct {
Scheme Scheme Scheme Scheme
ImageSource image.Source ImageSource image.Source
Location string Location string
Platform string
autoDetectAvailableImageSources bool autoDetectAvailableImageSources bool
} }
// ParseInput generates a source Input that can be used as an argument to generate a new source // ParseInput generates a source Input that can be used as an argument to generate a new source
// from specific providers including a registry. // from specific providers including a registry.
func ParseInput(userInput string, detectAvailableImageSources bool) (*Input, error) { func ParseInput(userInput string, platform string, detectAvailableImageSources bool) (*Input, error) {
fs := afero.NewOsFs() fs := afero.NewOsFs()
scheme, source, location, err := DetectScheme(fs, image.DetectSource, userInput) scheme, source, location, err := DetectScheme(fs, image.DetectSource, userInput)
if err != nil { if err != nil {
@ -71,12 +72,17 @@ func ParseInput(userInput string, detectAvailableImageSources bool) (*Input, err
} }
} }
if scheme != ImageScheme && platform != "" {
return nil, fmt.Errorf("cannot specify a platform for a non-image source")
}
// collect user input for downstream consumption // collect user input for downstream consumption
return &Input{ return &Input{
UserInput: userInput, UserInput: userInput,
Scheme: scheme, Scheme: scheme,
ImageSource: source, ImageSource: source,
Location: location, Location: location,
Platform: platform,
autoDetectAvailableImageSources: detectAvailableImageSources, autoDetectAvailableImageSources: detectAvailableImageSources,
}, nil }, nil
} }
@ -147,10 +153,19 @@ func getImageWithRetryStrategy(in Input, registryOptions *image.RegistryOptions)
opts = append(opts, stereoscope.WithRegistryOptions(*registryOptions)) opts = append(opts, stereoscope.WithRegistryOptions(*registryOptions))
} }
if in.Platform != "" {
opts = append(opts, stereoscope.WithPlatform(in.Platform))
}
img, err := stereoscope.GetImageFromSource(ctx, in.Location, in.ImageSource, opts...) img, err := stereoscope.GetImageFromSource(ctx, in.Location, in.ImageSource, opts...)
cleanup := func() {
if err := img.Cleanup(); err != nil {
log.Warnf("unable to cleanup image=%q: %w", in.UserInput, err)
}
}
if err == nil { if err == nil {
// Success on the first try! // Success on the first try!
return img, stereoscope.Cleanup, nil return img, cleanup, nil
} }
scheme := parseScheme(in.UserInput) scheme := parseScheme(in.UserInput)
@ -180,11 +195,12 @@ func getImageWithRetryStrategy(in Input, registryOptions *image.RegistryOptions)
in.ImageSource = image.DetermineDefaultImagePullSource(in.UserInput) in.ImageSource = image.DetermineDefaultImagePullSource(in.UserInput)
} }
img, err = stereoscope.GetImageFromSource(ctx, in.UserInput, in.ImageSource, opts...) img, err = stereoscope.GetImageFromSource(ctx, in.UserInput, in.ImageSource, opts...)
if err != nil { cleanup = func() {
return nil, nil, err if err := img.Cleanup(); err != nil {
log.Warnf("unable to cleanup image=%q: %w", in.UserInput, err)
} }
}
return img, stereoscope.Cleanup, nil return img, cleanup, err
} }
func generateDirectorySource(fs afero.Fs, location string) (*Source, func(), error) { func generateDirectorySource(fs afero.Fs, location string) (*Source, func(), error) {

View File

@ -26,22 +26,34 @@ func TestParseInput(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string input string
platform string
expected Scheme expected Scheme
errFn require.ErrorAssertionFunc
}{ }{
{ {
name: "ParseInput parses a file input", name: "ParseInput parses a file input",
input: "test-fixtures/image-simple/file-1.txt", input: "test-fixtures/image-simple/file-1.txt",
expected: FileScheme, expected: FileScheme,
}, },
{
name: "errors out when using platform for non-image scheme",
input: "test-fixtures/image-simple/file-1.txt",
platform: "arm64",
errFn: require.Error,
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
sourceInput, err := ParseInput(test.input, true) if test.errFn == nil {
if err != nil { test.errFn = require.NoError
t.Errorf("failed to ParseInput")
} }
sourceInput, err := ParseInput(test.input, test.platform, true)
test.errFn(t, err)
if test.expected != "" {
require.NotNil(t, sourceInput)
assert.Equal(t, sourceInput.Scheme, test.expected) assert.Equal(t, sourceInput.Scheme, test.expected)
}
}) })
} }
} }
@ -452,7 +464,7 @@ func TestDirectoryExclusions(t *testing.T) {
registryOpts := &image.RegistryOptions{} registryOpts := &image.RegistryOptions{}
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
sourceInput, err := ParseInput("dir:"+test.input, false) sourceInput, err := ParseInput("dir:"+test.input, "", false)
require.NoError(t, err) require.NoError(t, err)
src, fn, err := New(*sourceInput, registryOpts, test.exclusions) src, fn, err := New(*sourceInput, registryOpts, test.exclusions)
defer fn() defer fn()
@ -546,7 +558,7 @@ func TestImageExclusions(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
archiveLocation := imagetest.PrepareFixtureImage(t, "docker-archive", test.input) archiveLocation := imagetest.PrepareFixtureImage(t, "docker-archive", test.input)
sourceInput, err := ParseInput(archiveLocation, false) sourceInput, err := ParseInput(archiveLocation, "", false)
require.NoError(t, err) require.NoError(t, err)
src, fn, err := New(*sourceInput, registryOpts, test.exclusions) src, fn, err := New(*sourceInput, registryOpts, test.exclusions)
defer fn() defer fn()

View File

@ -184,6 +184,14 @@ func TestPackagesCmdFlags(t *testing.T) {
assertInOutput("search-indexed-archives: false"), assertInOutput("search-indexed-archives: false"),
}, },
}, },
{
name: "platform-option-wired-up",
args: []string{"packages", "--platform", "arm64", "-o", "json", "registry:busybox:1.31"},
assertions: []traitAssertion{
assertInOutput("sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853"), // linux/arm64 image digest
assertSuccessfulReturnCode,
},
},
} }
for _, test := range tests { for _, test := range tests {

View File

@ -24,7 +24,7 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
for _, c := range cataloger.ImageCatalogers(cataloger.DefaultConfig()) { for _, c := range cataloger.ImageCatalogers(cataloger.DefaultConfig()) {
// in case of future alteration where state is persisted, assume no dependency is safe to reuse // in case of future alteration where state is persisted, assume no dependency is safe to reuse
userInput := "docker-archive:" + tarPath userInput := "docker-archive:" + tarPath
sourceInput, err := source.ParseInput(userInput, false) sourceInput, err := source.ParseInput(userInput, "", false)
require.NoError(b, err) require.NoError(b, err)
theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) theSource, cleanupSource, err := source.New(*sourceInput, nil, nil)
b.Cleanup(cleanupSource) b.Cleanup(cleanupSource)

View File

@ -17,7 +17,7 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string) (sbom.SBOM, *sou
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName) imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
userInput := "docker-archive:" + tarPath userInput := "docker-archive:" + tarPath
sourceInput, err := source.ParseInput(userInput, false) sourceInput, err := source.ParseInput(userInput, "", false)
require.NoError(t, err) require.NoError(t, err)
theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) theSource, cleanupSource, err := source.New(*sourceInput, nil, nil)
t.Cleanup(cleanupSource) t.Cleanup(cleanupSource)
@ -52,7 +52,7 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string) (sbom.SBOM, *sou
func catalogDirectory(t *testing.T, dir string) (sbom.SBOM, *source.Source) { func catalogDirectory(t *testing.T, dir string) (sbom.SBOM, *source.Source) {
userInput := "dir:" + dir userInput := "dir:" + dir
sourceInput, err := source.ParseInput(userInput, false) sourceInput, err := source.ParseInput(userInput, "", false)
require.NoError(t, err) require.NoError(t, err)
theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) theSource, cleanupSource, err := source.New(*sourceInput, nil, nil)
t.Cleanup(cleanupSource) t.Cleanup(cleanupSource)