mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 02:26:42 +01:00
Add ability to pull images directly from a registry (#378)
* add registry image source Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * use explicit source for fetching image + add scheme and registry tests Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * adjust test variable name and add credential helper function Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
c02ab88d5f
commit
c363b2b532
30
README.md
30
README.md
@ -45,6 +45,16 @@ syft packages path/to/image.tar
|
||||
syft packages path/to/dir
|
||||
```
|
||||
|
||||
Sources can be explicitly provided with a scheme:
|
||||
```
|
||||
docker:yourrepo/yourimage:tag use images from the Docker daemon
|
||||
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
```
|
||||
|
||||
The output format for Syft is configurable as well:
|
||||
```
|
||||
syft packages <image> -o <format>
|
||||
@ -184,6 +194,26 @@ secrets:
|
||||
# SYFT_SECRETS_EXCLUDE_PATTERN_NAMES env var
|
||||
exclude-pattern-names: []
|
||||
|
||||
# options when pulling directly from a registry via the "registry:" scheme
|
||||
registry:
|
||||
# skip TLS verification when communicating with the registry
|
||||
# SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var
|
||||
insecure-skip-tls-verify: false
|
||||
|
||||
# credentials for specific registries
|
||||
auth:
|
||||
- # the URL to the registry (e.g. "docker.io", "localhost:5000", etc.)
|
||||
# SYFT_REGISTRY_AUTH_AUTHORITY env var
|
||||
authority: ""
|
||||
# SYFT_REGISTRY_AUTH_USERNAME env var
|
||||
username: ""
|
||||
# SYFT_REGISTRY_AUTH_PASSWORD env var
|
||||
password: ""
|
||||
# note: token and username/password are mutually exclusive
|
||||
# SYFT_REGISTRY_AUTH_TOKEN env var
|
||||
token: ""
|
||||
- ... # note, more credentials can be provided via config file only
|
||||
|
||||
log:
|
||||
# use structured logging
|
||||
# same as SYFT_LOG_STRUCTURED env var
|
||||
|
||||
@ -32,7 +32,7 @@ const (
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
||||
|
||||
You can also explicitly specify the scheme to use:
|
||||
@ -41,6 +41,7 @@ const (
|
||||
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
`
|
||||
)
|
||||
|
||||
@ -187,7 +188,7 @@ func packagesExecWorker(userInput string) <-chan error {
|
||||
|
||||
checkForApplicationUpdate()
|
||||
|
||||
src, cleanup, err := source.New(userInput)
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to determine image source: %+v", err)
|
||||
return
|
||||
|
||||
@ -81,7 +81,7 @@ func powerUserExecWorker(userInput string) <-chan error {
|
||||
|
||||
checkForApplicationUpdate()
|
||||
|
||||
src, cleanup, err := source.New(userInput)
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
|
||||
2
go.mod
2
go.mod
@ -10,7 +10,7 @@ require (
|
||||
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/stereoscope v0.0.0-20210405181843-73d71fd93233
|
||||
github.com/anchore/stereoscope v0.0.0-20210412194439-0b9e0281ef0c
|
||||
github.com/antihax/optional v1.0.0
|
||||
github.com/bmatcuk/doublestar/v2 v2.0.4
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
|
||||
|
||||
6
go.sum
6
go.sum
@ -115,10 +115,8 @@ github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0v
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/stereoscope v0.0.0-20210323182342-47b72675ff65 h1:r3tiir6UCgj/YeTqy4s2bfhZ9SuJYNlXx1Z9e/eLrbI=
|
||||
github.com/anchore/stereoscope v0.0.0-20210323182342-47b72675ff65/go.mod h1:G7tFR0iI9r6AvibmXKA9v010pRS1IIJgd0t6fOMDxCw=
|
||||
github.com/anchore/stereoscope v0.0.0-20210405181843-73d71fd93233 h1:XkoyUFdQGYT2tb7SH2YBsouw/9q1kZTgXVy52PzM4JE=
|
||||
github.com/anchore/stereoscope v0.0.0-20210405181843-73d71fd93233/go.mod h1:G7tFR0iI9r6AvibmXKA9v010pRS1IIJgd0t6fOMDxCw=
|
||||
github.com/anchore/stereoscope v0.0.0-20210412194439-0b9e0281ef0c h1:iAkv8iBnbHQzcROt55IbEh7r7qUJxj64E8bM4EnaBlA=
|
||||
github.com/anchore/stereoscope v0.0.0-20210412194439-0b9e0281ef0c/go.mod h1:vhh1M99rfWx5ejMvz1lkQiFZUrC5wu32V12R4JXH+ZI=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
|
||||
@ -40,6 +40,7 @@ type Application struct {
|
||||
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
|
||||
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
|
||||
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
}
|
||||
|
||||
func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
|
||||
|
||||
72
internal/config/registry.go
Normal file
72
internal/config/registry.go
Normal file
@ -0,0 +1,72 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type RegistryCredentials struct {
|
||||
Authority string `yaml:"authority" json:"authority" mapstructure:"authority"`
|
||||
// IMPORTANT: do not show the username in any YAML/JSON output (sensitive information)
|
||||
Username string `yaml:"-" json:"-" mapstructure:"username"`
|
||||
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
|
||||
Password string `yaml:"-" json:"-" mapstructure:"password"`
|
||||
// IMPORTANT: do not show the token in any YAML/JSON output (sensitive information)
|
||||
Token string `yaml:"-" json:"-" mapstructure:"token"`
|
||||
}
|
||||
|
||||
type registry struct {
|
||||
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
|
||||
Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
|
||||
}
|
||||
|
||||
func (cfg registry) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("registry.insecure-skip-tls-verify", false)
|
||||
v.SetDefault("registry.auth", []RegistryCredentials{})
|
||||
}
|
||||
|
||||
// nolint: unparam
|
||||
func (cfg *registry) parseConfigValues() error {
|
||||
// there may be additional credentials provided by env var that should be appended to the set of credentials
|
||||
authority, username, password, token :=
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_AUTHORITY"),
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_USERNAME"),
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"),
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_TOKEN")
|
||||
|
||||
if hasNonEmptyCredentials(authority, password, token) {
|
||||
// note: we prepend the credentials such that the environment variables take precedence over on-disk configuration.
|
||||
cfg.Auth = append([]RegistryCredentials{
|
||||
{
|
||||
Authority: authority,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Token: token,
|
||||
},
|
||||
}, cfg.Auth...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasNonEmptyCredentials(authority, password, token string) bool {
|
||||
return authority != "" && password != "" || authority != "" && token != ""
|
||||
}
|
||||
|
||||
func (cfg *registry) ToOptions() *image.RegistryOptions {
|
||||
var auth = make([]image.RegistryCredentials, len(cfg.Auth))
|
||||
for i, a := range cfg.Auth {
|
||||
auth[i] = image.RegistryCredentials{
|
||||
Authority: a.Authority,
|
||||
Username: a.Username,
|
||||
Password: a.Password,
|
||||
Token: a.Token,
|
||||
}
|
||||
}
|
||||
return &image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
|
||||
Credentials: auth,
|
||||
}
|
||||
}
|
||||
@ -223,6 +223,10 @@
|
||||
"exclude-pattern-names": null,
|
||||
"reveal-values": false,
|
||||
"skip-files-above-size": 0
|
||||
},
|
||||
"registry": {
|
||||
"insecure-skip-tls-verify": false,
|
||||
"auth": null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -21,39 +21,39 @@ const (
|
||||
ImageScheme Scheme = "ImageScheme"
|
||||
)
|
||||
|
||||
func detectScheme(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, string, error) {
|
||||
func detectScheme(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, image.Source, string, error) {
|
||||
if strings.HasPrefix(userInput, "dir:") {
|
||||
// blindly trust the user's scheme
|
||||
dirLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "dir:"))
|
||||
if err != nil {
|
||||
return UnknownScheme, "", fmt.Errorf("unable to expand directory path: %w", err)
|
||||
return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to expand directory path: %w", err)
|
||||
}
|
||||
return DirectoryScheme, dirLocation, nil
|
||||
return DirectoryScheme, image.UnknownSource, dirLocation, nil
|
||||
}
|
||||
|
||||
// we should attempt to let stereoscope determine what the source is first --just because the source is a valid directory
|
||||
// we should attempt to let stereoscope determine what the source is first --but, just because the source is a valid directory
|
||||
// doesn't mean we yet know if it is an OCI layout directory (to be treated as an image) or if it is a generic filesystem directory.
|
||||
source, imageSpec, err := imageDetector(userInput)
|
||||
if err != nil {
|
||||
return UnknownScheme, "", fmt.Errorf("unable to detect the scheme from %q: %w", userInput, err)
|
||||
return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to detect the scheme from %q: %w", userInput, err)
|
||||
}
|
||||
|
||||
if source == image.UnknownSource {
|
||||
dirLocation, err := homedir.Expand(userInput)
|
||||
if err != nil {
|
||||
return UnknownScheme, "", fmt.Errorf("unable to expand potential directory path: %w", err)
|
||||
return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to expand potential directory path: %w", err)
|
||||
}
|
||||
|
||||
fileMeta, err := fs.Stat(dirLocation)
|
||||
if err != nil {
|
||||
return UnknownScheme, "", nil
|
||||
return UnknownScheme, source, "", nil
|
||||
}
|
||||
|
||||
if fileMeta.IsDir() {
|
||||
return DirectoryScheme, dirLocation, nil
|
||||
return DirectoryScheme, source, dirLocation, nil
|
||||
}
|
||||
return UnknownScheme, "", nil
|
||||
return UnknownScheme, source, "", nil
|
||||
}
|
||||
|
||||
return ImageScheme, imageSpec, nil
|
||||
return ImageScheme, source, imageSpec, nil
|
||||
}
|
||||
|
||||
263
syft/source/scheme_test.go
Normal file
263
syft/source/scheme_test.go
Normal file
@ -0,0 +1,263 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDetectScheme(t *testing.T) {
|
||||
type detectorResult struct {
|
||||
src image.Source
|
||||
ref string
|
||||
err error
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
userInput string
|
||||
dirs []string
|
||||
detection detectorResult
|
||||
expectedScheme Scheme
|
||||
expectedLocation string
|
||||
}{
|
||||
{
|
||||
name: "docker-image-ref",
|
||||
userInput: "wagoodman/dive:latest",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "wagoodman/dive:latest",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "wagoodman/dive:latest",
|
||||
},
|
||||
{
|
||||
name: "docker-image-ref-no-tag",
|
||||
userInput: "wagoodman/dive",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "wagoodman/dive",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "wagoodman/dive",
|
||||
},
|
||||
{
|
||||
name: "registry-image-explicit-scheme",
|
||||
userInput: "registry:wagoodman/dive:latest",
|
||||
detection: detectorResult{
|
||||
src: image.OciRegistrySource,
|
||||
ref: "wagoodman/dive:latest",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "wagoodman/dive:latest",
|
||||
},
|
||||
{
|
||||
name: "docker-image-explicit-scheme",
|
||||
userInput: "docker:wagoodman/dive:latest",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "wagoodman/dive:latest",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "wagoodman/dive:latest",
|
||||
},
|
||||
{
|
||||
name: "docker-image-explicit-scheme-no-tag",
|
||||
userInput: "docker:wagoodman/dive",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "wagoodman/dive",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "wagoodman/dive",
|
||||
},
|
||||
{
|
||||
name: "docker-image-edge-case",
|
||||
userInput: "docker:latest",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "latest",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
// we want to be able to handle this case better, however, I don't see a way to do this
|
||||
// the user will need to provide more explicit input (docker:docker:latest)
|
||||
expectedLocation: "latest",
|
||||
},
|
||||
{
|
||||
name: "docker-image-edge-case-explicit",
|
||||
userInput: "docker:docker:latest",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "docker:latest",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
// we want to be able to handle this case better, however, I don't see a way to do this
|
||||
// the user will need to provide more explicit input (docker:docker:latest)
|
||||
expectedLocation: "docker:latest",
|
||||
},
|
||||
{
|
||||
name: "oci-tar",
|
||||
userInput: "some/path-to-file",
|
||||
detection: detectorResult{
|
||||
src: image.OciTarballSource,
|
||||
ref: "some/path-to-file",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "some/path-to-file",
|
||||
},
|
||||
{
|
||||
name: "oci-dir",
|
||||
userInput: "some/path-to-dir",
|
||||
detection: detectorResult{
|
||||
src: image.OciDirectorySource,
|
||||
ref: "some/path-to-dir",
|
||||
},
|
||||
dirs: []string{"some/path-to-dir"},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "some/path-to-dir",
|
||||
},
|
||||
{
|
||||
name: "guess-dir",
|
||||
userInput: "some/path-to-dir",
|
||||
detection: detectorResult{
|
||||
src: image.UnknownSource,
|
||||
ref: "",
|
||||
},
|
||||
dirs: []string{"some/path-to-dir"},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: "some/path-to-dir",
|
||||
},
|
||||
{
|
||||
name: "generic-dir-does-not-exist",
|
||||
userInput: "some/path-to-dir",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "some/path-to-dir",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "some/path-to-dir",
|
||||
},
|
||||
{
|
||||
name: "explicit-dir",
|
||||
userInput: "dir:some/path-to-dir",
|
||||
detection: detectorResult{
|
||||
src: image.UnknownSource,
|
||||
ref: "",
|
||||
},
|
||||
dirs: []string{"some/path-to-dir"},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: "some/path-to-dir",
|
||||
},
|
||||
{
|
||||
name: "explicit-current-dir",
|
||||
userInput: "dir:.",
|
||||
detection: detectorResult{
|
||||
src: image.UnknownSource,
|
||||
ref: "",
|
||||
},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: ".",
|
||||
},
|
||||
{
|
||||
name: "current-dir",
|
||||
userInput: ".",
|
||||
detection: detectorResult{
|
||||
src: image.UnknownSource,
|
||||
ref: "",
|
||||
},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: ".",
|
||||
},
|
||||
// we should support tilde expansion
|
||||
{
|
||||
name: "tilde-expansion-image-implicit",
|
||||
userInput: "~/some-path",
|
||||
detection: detectorResult{
|
||||
src: image.OciDirectorySource,
|
||||
ref: "~/some-path",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "~/some-path",
|
||||
},
|
||||
{
|
||||
name: "tilde-expansion-dir-implicit",
|
||||
userInput: "~/some-path",
|
||||
detection: detectorResult{
|
||||
src: image.UnknownSource,
|
||||
ref: "",
|
||||
},
|
||||
dirs: []string{"~/some-path"},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: "~/some-path",
|
||||
},
|
||||
{
|
||||
name: "tilde-expansion-dir-explicit-exists",
|
||||
userInput: "dir:~/some-path",
|
||||
dirs: []string{"~/some-path"},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: "~/some-path",
|
||||
},
|
||||
{
|
||||
name: "tilde-expansion-dir-explicit-dne",
|
||||
userInput: "dir:~/some-path",
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: "~/some-path",
|
||||
},
|
||||
{
|
||||
name: "tilde-expansion-dir-implicit-dne",
|
||||
userInput: "~/some-path",
|
||||
expectedScheme: UnknownScheme,
|
||||
expectedLocation: "",
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
for _, p := range test.dirs {
|
||||
expandedExpectedLocation, err := homedir.Expand(p)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to expand path=%q: %+v", p, err)
|
||||
}
|
||||
err = fs.Mkdir(expandedExpectedLocation, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create dummy tar: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
imageDetector := func(string) (image.Source, string, error) {
|
||||
// lean on the users real home directory value
|
||||
switch test.detection.src {
|
||||
case image.OciDirectorySource, image.DockerTarballSource, image.OciTarballSource:
|
||||
expandedExpectedLocation, err := homedir.Expand(test.expectedLocation)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to expand path=%q: %+v", test.expectedLocation, err)
|
||||
}
|
||||
return test.detection.src, expandedExpectedLocation, test.detection.err
|
||||
default:
|
||||
return test.detection.src, test.detection.ref, test.detection.err
|
||||
}
|
||||
}
|
||||
|
||||
actualScheme, actualSource, actualLocation, err := detectScheme(fs, imageDetector, test.userInput)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err : %+v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.detection.src, actualSource, "mismatched source")
|
||||
assert.Equal(t, test.expectedScheme, actualScheme, "mismatched scheme")
|
||||
|
||||
// lean on the users real home directory value
|
||||
expandedExpectedLocation, err := homedir.Expand(test.expectedLocation)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to expand path=%q: %+v", test.expectedLocation, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, expandedExpectedLocation, actualLocation, "mismatched location")
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -8,11 +8,9 @@ package source
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Source is an object that captures the data source to be cataloged, configuration, and a specific resolver used
|
||||
@ -25,9 +23,9 @@ type Source struct {
|
||||
type sourceDetector func(string) (image.Source, string, error)
|
||||
|
||||
// New produces a Source based on userInput like dir: or image:tag
|
||||
func New(userInput string) (Source, func(), error) {
|
||||
func New(userInput string, registryOptions *image.RegistryOptions) (Source, func(), error) {
|
||||
fs := afero.NewOsFs()
|
||||
parsedScheme, location, err := detectScheme(fs, image.DetectSource, userInput)
|
||||
parsedScheme, imageSource, location, err := detectScheme(fs, image.DetectSource, userInput)
|
||||
if err != nil {
|
||||
return Source{}, func() {}, fmt.Errorf("unable to parse input=%q: %w", userInput, err)
|
||||
}
|
||||
@ -50,7 +48,7 @@ func New(userInput string) (Source, func(), error) {
|
||||
return s, func() {}, nil
|
||||
|
||||
case ImageScheme:
|
||||
img, err := stereoscope.GetImage(location)
|
||||
img, err := stereoscope.GetImageFromSource(location, imageSource, registryOptions)
|
||||
cleanup := func() {
|
||||
stereoscope.Cleanup()
|
||||
}
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestNewFromImageFails(t *testing.T) {
|
||||
@ -173,248 +170,3 @@ func TestFilesByGlob(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectScheme(t *testing.T) {
|
||||
type detectorResult struct {
|
||||
src image.Source
|
||||
ref string
|
||||
err error
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
userInput string
|
||||
dirs []string
|
||||
detection detectorResult
|
||||
expectedScheme Scheme
|
||||
expectedLocation string
|
||||
}{
|
||||
{
|
||||
name: "docker-image-ref",
|
||||
userInput: "wagoodman/dive:latest",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "wagoodman/dive:latest",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "wagoodman/dive:latest",
|
||||
},
|
||||
{
|
||||
name: "docker-image-ref-no-tag",
|
||||
userInput: "wagoodman/dive",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "wagoodman/dive",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "wagoodman/dive",
|
||||
},
|
||||
{
|
||||
name: "docker-image-explicit-scheme",
|
||||
userInput: "docker:wagoodman/dive:latest",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "wagoodman/dive:latest",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "wagoodman/dive:latest",
|
||||
},
|
||||
{
|
||||
name: "docker-image-explicit-scheme-no-tag",
|
||||
userInput: "docker:wagoodman/dive",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "wagoodman/dive",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "wagoodman/dive",
|
||||
},
|
||||
{
|
||||
name: "docker-image-edge-case",
|
||||
userInput: "docker:latest",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "latest",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
// we want to be able to handle this case better, however, I don't see a way to do this
|
||||
// the user will need to provide more explicit input (docker:docker:latest)
|
||||
expectedLocation: "latest",
|
||||
},
|
||||
{
|
||||
name: "docker-image-edge-case-explicit",
|
||||
userInput: "docker:docker:latest",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "docker:latest",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
// we want to be able to handle this case better, however, I don't see a way to do this
|
||||
// the user will need to provide more explicit input (docker:docker:latest)
|
||||
expectedLocation: "docker:latest",
|
||||
},
|
||||
{
|
||||
name: "oci-tar",
|
||||
userInput: "some/path-to-file",
|
||||
detection: detectorResult{
|
||||
src: image.OciTarballSource,
|
||||
ref: "some/path-to-file",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "some/path-to-file",
|
||||
},
|
||||
{
|
||||
name: "oci-dir",
|
||||
userInput: "some/path-to-dir",
|
||||
detection: detectorResult{
|
||||
src: image.OciDirectorySource,
|
||||
ref: "some/path-to-dir",
|
||||
},
|
||||
dirs: []string{"some/path-to-dir"},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "some/path-to-dir",
|
||||
},
|
||||
{
|
||||
name: "guess-dir",
|
||||
userInput: "some/path-to-dir",
|
||||
detection: detectorResult{
|
||||
src: image.UnknownSource,
|
||||
ref: "",
|
||||
},
|
||||
dirs: []string{"some/path-to-dir"},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: "some/path-to-dir",
|
||||
},
|
||||
{
|
||||
name: "generic-dir-does-not-exist",
|
||||
userInput: "some/path-to-dir",
|
||||
detection: detectorResult{
|
||||
src: image.DockerDaemonSource,
|
||||
ref: "some/path-to-dir",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "some/path-to-dir",
|
||||
},
|
||||
{
|
||||
name: "explicit-dir",
|
||||
userInput: "dir:some/path-to-dir",
|
||||
detection: detectorResult{
|
||||
src: image.UnknownSource,
|
||||
ref: "",
|
||||
},
|
||||
dirs: []string{"some/path-to-dir"},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: "some/path-to-dir",
|
||||
},
|
||||
{
|
||||
name: "explicit-current-dir",
|
||||
userInput: "dir:.",
|
||||
detection: detectorResult{
|
||||
src: image.UnknownSource,
|
||||
ref: "",
|
||||
},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: ".",
|
||||
},
|
||||
{
|
||||
name: "current-dir",
|
||||
userInput: ".",
|
||||
detection: detectorResult{
|
||||
src: image.UnknownSource,
|
||||
ref: "",
|
||||
},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: ".",
|
||||
},
|
||||
// we should support tilde expansion
|
||||
{
|
||||
name: "tilde-expansion-image-implicit",
|
||||
userInput: "~/some-path",
|
||||
detection: detectorResult{
|
||||
src: image.OciDirectorySource,
|
||||
ref: "~/some-path",
|
||||
},
|
||||
expectedScheme: ImageScheme,
|
||||
expectedLocation: "~/some-path",
|
||||
},
|
||||
{
|
||||
name: "tilde-expansion-dir-implicit",
|
||||
userInput: "~/some-path",
|
||||
detection: detectorResult{
|
||||
src: image.UnknownSource,
|
||||
ref: "",
|
||||
},
|
||||
dirs: []string{"~/some-path"},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: "~/some-path",
|
||||
},
|
||||
{
|
||||
name: "tilde-expansion-dir-explicit-exists",
|
||||
userInput: "dir:~/some-path",
|
||||
dirs: []string{"~/some-path"},
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: "~/some-path",
|
||||
},
|
||||
{
|
||||
name: "tilde-expansion-dir-explicit-dne",
|
||||
userInput: "dir:~/some-path",
|
||||
expectedScheme: DirectoryScheme,
|
||||
expectedLocation: "~/some-path",
|
||||
},
|
||||
{
|
||||
name: "tilde-expansion-dir-implicit-dne",
|
||||
userInput: "~/some-path",
|
||||
expectedScheme: UnknownScheme,
|
||||
expectedLocation: "",
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
for _, p := range test.dirs {
|
||||
expandedExpectedLocation, err := homedir.Expand(p)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to expand path=%q: %+v", p, err)
|
||||
}
|
||||
err = fs.Mkdir(expandedExpectedLocation, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create dummy tar: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
imageDetector := func(string) (image.Source, string, error) {
|
||||
// lean on the users real home directory value
|
||||
switch test.detection.src {
|
||||
case image.OciDirectorySource, image.DockerTarballSource, image.OciTarballSource:
|
||||
expandedExpectedLocation, err := homedir.Expand(test.expectedLocation)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to expand path=%q: %+v", test.expectedLocation, err)
|
||||
}
|
||||
return test.detection.src, expandedExpectedLocation, test.detection.err
|
||||
default:
|
||||
return test.detection.src, test.detection.ref, test.detection.err
|
||||
}
|
||||
}
|
||||
|
||||
actualScheme, actualLocation, err := detectScheme(fs, imageDetector, test.userInput)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err : %+v", err)
|
||||
}
|
||||
|
||||
if actualScheme != test.expectedScheme {
|
||||
t.Errorf("expected scheme %q , got %q", test.expectedScheme, actualScheme)
|
||||
}
|
||||
|
||||
// lean on the users real home directory value
|
||||
expandedExpectedLocation, err := homedir.Expand(test.expectedLocation)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to expand path=%q: %+v", test.expectedLocation, err)
|
||||
}
|
||||
|
||||
if actualLocation != expandedExpectedLocation {
|
||||
t.Errorf("expected location %q , got %q", expandedExpectedLocation, actualLocation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ func TestPackagesCmdFlags(t *testing.T) {
|
||||
args: []string{"packages", "-o", "json", request},
|
||||
assertions: []traitAssertion{
|
||||
assertJsonReport,
|
||||
assertSource(source.SquashedScope),
|
||||
assertScope(source.SquashedScope),
|
||||
assertSuccessfulReturnCode,
|
||||
},
|
||||
},
|
||||
@ -56,7 +56,7 @@ func TestPackagesCmdFlags(t *testing.T) {
|
||||
name: "squashed-scope-flag",
|
||||
args: []string{"packages", "-o", "json", "-s", "squashed", request},
|
||||
assertions: []traitAssertion{
|
||||
assertSource(source.SquashedScope),
|
||||
assertScope(source.SquashedScope),
|
||||
assertSuccessfulReturnCode,
|
||||
},
|
||||
},
|
||||
@ -64,7 +64,7 @@ func TestPackagesCmdFlags(t *testing.T) {
|
||||
name: "all-layers-scope-flag",
|
||||
args: []string{"packages", "-o", "json", "-s", "all-layers", request},
|
||||
assertions: []traitAssertion{
|
||||
assertSource(source.AllLayersScope),
|
||||
assertScope(source.AllLayersScope),
|
||||
assertSuccessfulReturnCode,
|
||||
},
|
||||
},
|
||||
@ -75,7 +75,7 @@ func TestPackagesCmdFlags(t *testing.T) {
|
||||
},
|
||||
args: []string{"packages", "-o", "json", request},
|
||||
assertions: []traitAssertion{
|
||||
assertSource(source.AllLayersScope),
|
||||
assertScope(source.AllLayersScope),
|
||||
assertSuccessfulReturnCode,
|
||||
},
|
||||
},
|
||||
@ -137,3 +137,75 @@ func TestPackagesCmdFlags(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryAuth(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
env map[string]string
|
||||
assertions []traitAssertion
|
||||
}{
|
||||
{
|
||||
name: "fallback to keychain",
|
||||
args: []string{"packages", "-vv", "registry:localhost:5000/something:latest"},
|
||||
assertions: []traitAssertion{
|
||||
assertInOutput("source=OciRegistry"),
|
||||
assertInOutput("localhost:5000/something:latest"),
|
||||
assertInOutput("no registry credentials configured, using the default keychain"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "use creds",
|
||||
args: []string{"packages", "-vv", "registry:localhost:5000/something:latest"},
|
||||
env: map[string]string{
|
||||
"SYFT_REGISTRY_AUTH_AUTHORITY": "localhost:5000",
|
||||
"SYFT_REGISTRY_AUTH_USERNAME": "username",
|
||||
"SYFT_REGISTRY_AUTH_PASSWORD": "password",
|
||||
},
|
||||
assertions: []traitAssertion{
|
||||
assertInOutput("source=OciRegistry"),
|
||||
assertInOutput("localhost:5000/something:latest"),
|
||||
assertInOutput(`using registry credentials for "localhost:5000"`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "use token",
|
||||
args: []string{"packages", "-vv", "registry:localhost:5000/something:latest"},
|
||||
env: map[string]string{
|
||||
"SYFT_REGISTRY_AUTH_AUTHORITY": "localhost:5000",
|
||||
"SYFT_REGISTRY_AUTH_TOKEN": "token",
|
||||
},
|
||||
assertions: []traitAssertion{
|
||||
assertInOutput("source=OciRegistry"),
|
||||
assertInOutput("localhost:5000/something:latest"),
|
||||
assertInOutput(`using registry token for "localhost:5000"`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not enough info fallsback to keychain",
|
||||
args: []string{"packages", "-vv", "registry:localhost:5000/something:latest"},
|
||||
env: map[string]string{
|
||||
"SYFT_REGISTRY_AUTH_AUTHORITY": "localhost:5000",
|
||||
},
|
||||
assertions: []traitAssertion{
|
||||
assertInOutput("source=OciRegistry"),
|
||||
assertInOutput("localhost:5000/something:latest"),
|
||||
assertInOutput(`no registry credentials configured, using the default keychain`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cmd, stdout, stderr := runSyftCommand(t, test.env, test.args...)
|
||||
for _, traitAssertionFn := range test.assertions {
|
||||
traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
if t.Failed() {
|
||||
t.Log("STDOUT:\n", stdout)
|
||||
t.Log("STDERR:\n", stderr)
|
||||
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ func assertTableReport(tb testing.TB, stdout, _ string, _ int) {
|
||||
}
|
||||
}
|
||||
|
||||
func assertSource(scope source.Scope) traitAssertion {
|
||||
func assertScope(scope source.Scope) traitAssertion {
|
||||
return func(tb testing.TB, stdout, stderr string, rc int) {
|
||||
// we can only verify source with the json report
|
||||
assertJsonReport(tb, stdout, stderr, rc)
|
||||
|
||||
@ -23,7 +23,7 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
|
||||
var pc *pkg.Catalog
|
||||
for _, c := range cataloger.ImageCatalogers() {
|
||||
// in case of future alteration where state is persisted, assume no dependency is safe to reuse
|
||||
theSource, cleanupSource, err := source.New("docker-archive:" + tarPath)
|
||||
theSource, cleanupSource, err := source.New("docker-archive:"+tarPath, nil)
|
||||
b.Cleanup(cleanupSource)
|
||||
if err != nil {
|
||||
b.Fatalf("unable to get source: %+v", err)
|
||||
|
||||
@ -14,7 +14,7 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string) (*pkg.Catalog, *
|
||||
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
|
||||
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
|
||||
|
||||
theSource, cleanupSource, err := source.New("docker-archive:" + tarPath)
|
||||
theSource, cleanupSource, err := source.New("docker-archive:"+tarPath, nil)
|
||||
t.Cleanup(cleanupSource)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get source: %+v", err)
|
||||
@ -29,7 +29,7 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string) (*pkg.Catalog, *
|
||||
}
|
||||
|
||||
func catalogDirectory(t *testing.T, dir string) (*pkg.Catalog, *distro.Distro, source.Source) {
|
||||
theSource, cleanupSource, err := source.New("dir:" + dir)
|
||||
theSource, cleanupSource, err := source.New("dir:"+dir, nil)
|
||||
t.Cleanup(cleanupSource)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get source: %+v", err)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user