diff --git a/common.go b/common.go index 6e3d526..362c0b5 100644 --- a/common.go +++ b/common.go @@ -32,6 +32,7 @@ const ( GitURI URIType = iota HTTPURI FileURI + OCIURI Unknown ) @@ -39,7 +40,7 @@ var getHomeDir = os.UserHomeDir // String returns the string representation of the URLType func (t URIType) String() string { - return [...]string{"GitURI", "HTTPURI", "FileURI", "Unknown"}[t] + return [...]string{"GitURI", "HTTPURI", "FileURI", "OCIURL", "Unknown"}[t] } // ExpandTilde expands a leading tilde in the file path to the user's home directory @@ -67,6 +68,10 @@ func ClassifyURI(input string) (URIType, error) { return HTTPURI, nil } + if strings.HasPrefix(input, "oci::") { + return OCIURI, nil + } + if strings.HasPrefix(input, "github.com") || strings.HasPrefix(input, "gitlab.com") { return GitURI, nil } @@ -77,6 +82,9 @@ func ClassifyURI(input string) (URIType, error) { httpURIPattern := regexp.MustCompile(`^((http://|https://)[\w\-]+(\.[\w\-]+)+.*)$`) // Regular expression for file paths filePathPattern := regexp.MustCompile(`^(\./|\../|/|[a-zA-Z]:\\|~\/|file://).*`) + // Regular expression for OCI URIs + ociURIPattern := regexp.MustCompile(`^((oci://)[\w\-]+(\.[\w\-]+)+.*)$`) + // Regular expressions for known OCI registries // Check if the input matches the file path pattern first if filePathPattern.MatchString(input) { @@ -103,6 +111,17 @@ func ClassifyURI(input string) (URIType, error) { } } + // Check if the input matches the OCI URI pattern + if ociURIPattern.MatchString(input) { + return OCIURI, nil + } + + // Check if the input matches any known OCI registry + isOCI := containsOCIRegistry(input) + if isOCI { + return OCIURI, nil + } + // Check for unsupported schemes parsedURI, err := url.Parse(input) if err == nil && parsedURI.Scheme != "" && parsedURI.Scheme != "http" && parsedURI.Scheme != "https" { @@ -131,3 +150,23 @@ func ValidateFileDestination(destination string) error { } return nil } + +// containsOCIRegistry checks if the input string contains a known OCI registry +func containsOCIRegistry(src string) bool { + matchRegistries := []*regexp.Regexp{ + regexp.MustCompile("azurecr.io"), + regexp.MustCompile("gcr.io"), + regexp.MustCompile("registry.gitlab.com"), + regexp.MustCompile("pkg.dev"), + regexp.MustCompile("[0-9]{12}.dkr.ecr.[a-z0-9-]*.amazonaws.com"), + regexp.MustCompile("^quay.io"), + regexp.MustCompile(`(?:::1|127\.0\.0\.1|(?i:localhost)):\d{1,5}`), // localhost OCI registry + } + + for _, matchRegistry := range matchRegistries { + if matchRegistry.MatchString(src) { + return true + } + } + return false +} diff --git a/common_test.go b/common_test.go index 48b157d..d07ad8d 100644 --- a/common_test.go +++ b/common_test.go @@ -97,6 +97,17 @@ func TestClassifyURI(t *testing.T) { {input: "ftpexamplecom", expected: Unknown}, {input: "github.com/user/repo.git", expected: GitURI}, {input: "gitlab.com/user/repo.git", expected: GitURI}, + {input: "oci::registry.gitlab.com/user/repo:latest", expected: OCIURI}, + {input: "oci::registry.gitlab.com/user/repo", expected: OCIURI}, + {input: "oci::registry.gitlab.com/user/repo:1.0.0", expected: OCIURI}, + {input: "oci://example.org/user/repo:latest", expected: OCIURI}, + {input: "quay.io/user/repo:latest", expected: OCIURI}, + {input: "127.0.0.1:5000", expected: OCIURI}, + {input: "registry.gitlab.com/user/repo:latest", expected: OCIURI}, + {input: "pkg.dev/user/repo:latest", expected: OCIURI}, + {input: "123456789012.dkr.ecr.us-west-2.amazonaws.com/user/repo:latest", expected: OCIURI}, + {input: "gcr.io/user/repo:latest", expected: OCIURI}, + {input: "azurecr.io/user/repo:latest", expected: OCIURI}, } for _, tc := range testCases { @@ -184,3 +195,30 @@ func TestValidateFileDestination_errors(t *testing.T) { os.RemoveAll(dir) }) } + +func TestContainsOCIRegistry(t *testing.T) { + testCases := []struct { + input string + expected bool + }{ + {input: "azurecr.io", expected: true}, + {input: "gcr.io", expected: true}, + {input: "registry.gitlab.com", expected: true}, + {input: "pkg.dev", expected: true}, + {input: "123456789012.dkr.ecr.us-west-2.amazonaws.com", expected: true}, + {input: "quay.io", expected: true}, + {input: "::1", expected: false}, + {input: "127.0.0.1", expected: false}, + {input: "123.123.123.123", expected: false}, + {input: "127.0.0.1:8080", expected: true}, + {input: "localhost:8080", expected: true}, + {input: "example.com", expected: false}, + } + + for _, tc := range testCases { + actual := containsOCIRegistry(tc.input) + if actual != tc.expected { + t.Errorf("Expected containsOCIRegistry(%s) to return %t, but got %t", tc.input, tc.expected, actual) + } + } +} diff --git a/gather/gather.go b/gather/gather.go index 7657ba1..53b5daf 100644 --- a/gather/gather.go +++ b/gather/gather.go @@ -28,6 +28,7 @@ import ( "github.com/enterprise-contract/go-gather/gather/file" "github.com/enterprise-contract/go-gather/gather/git" "github.com/enterprise-contract/go-gather/gather/http" + "github.com/enterprise-contract/go-gather/gather/oci" "github.com/enterprise-contract/go-gather/metadata" ) @@ -41,6 +42,7 @@ var protocolHandlers = map[string]Gatherer{ "FileURI": &file.FileGatherer{}, "GitURI": &git.GitGatherer{}, "HTTPURI": &http.HTTPGatherer{}, + "OCIURI": &oci.OCIGatherer{}, } // Gather determines the protocol from the source URI and uses the appropriate Gatherer to perform the operation. diff --git a/gather/gather_test.go b/gather/gather_test.go index 87c14f6..218d660 100644 --- a/gather/gather_test.go +++ b/gather/gather_test.go @@ -32,7 +32,7 @@ func TestGather(t *testing.T) { ctx := context.Background() t.Run("SourceParseError", func(t *testing.T) { source := ":" - destination := "/path/to/destination" + destination := "/tmp/foo" _, err := Gather(ctx, source, destination) if err == nil { @@ -43,11 +43,14 @@ func TestGather(t *testing.T) { if err.Error() != expectedErrorMessage { t.Errorf("expected error message: %s, but got: %s", expectedErrorMessage, err.Error()) } + t.Cleanup(func() { + os.RemoveAll(destination) + }) }) t.Run("UnsupportedProtocol", func(t *testing.T) { source := "ftp://example.com/file.txt" - destination := "/path/to/destination" + destination := "/tmp/foo" defer os.RemoveAll(destination) _, err := Gather(ctx, source, destination) @@ -66,7 +69,7 @@ func TestGather(t *testing.T) { t.Run("SupportedProtocol_git", func(t *testing.T) { source := "git::https://github.com/git-fixtures/basic.git" - destination := "/tmp/path/to/destination" + destination := "/tmp/foo" defer os.RemoveAll(destination) _, err := Gather(ctx, source, destination) @@ -91,6 +94,9 @@ func TestGather(t *testing.T) { if err != nil { t.Errorf("expected no error, but got: %s", err.Error()) } + t.Cleanup(func() { + os.RemoveAll(destination) + }) }) t.Run("CustomGatherer", func(t *testing.T) { @@ -102,6 +108,9 @@ func TestGather(t *testing.T) { if err != nil { t.Errorf("expected no error, but got: %s", err.Error()) } + t.Cleanup(func() { + os.RemoveAll(destination) + }) }) } diff --git a/gather/go.mod b/gather/go.mod index 4096f82..d4a966e 100644 --- a/gather/go.mod +++ b/gather/go.mod @@ -7,6 +7,7 @@ require ( github.com/enterprise-contract/go-gather/gather/file v0.0.1 github.com/enterprise-contract/go-gather/gather/git v0.0.1 github.com/enterprise-contract/go-gather/gather/http v0.0.1 + github.com/enterprise-contract/go-gather/gather/oci v0.0.1 github.com/enterprise-contract/go-gather/metadata v0.0.1 github.com/enterprise-contract/go-gather/metadata/git v0.0.1 ) diff --git a/gather/oci/go.mod b/gather/oci/go.mod new file mode 100644 index 0000000..57b8477 --- /dev/null +++ b/gather/oci/go.mod @@ -0,0 +1,39 @@ +module github.com/enterprise-contract/go-gather/gather/oci + +go 1.21.9 + +require ( + github.com/open-policy-agent/conftest v0.53.0 + oras.land/oras-go/v2 v2.5.0 + + github.com/enterprise-contract/go-gather/metadata/oci v0.0.1 +) + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/google/go-containerregistry v0.19.2 + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) \ No newline at end of file diff --git a/gather/oci/go.sum b/gather/oci/go.sum new file mode 100644 index 0000000..44775cb --- /dev/null +++ b/gather/oci/go.sum @@ -0,0 +1,80 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w= +github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/open-policy-agent/conftest v0.53.0 h1:tLj0NMOBMmxnr1wTpNIuhCyuLf6D4+ksti56OX7wzAY= +github.com/open-policy-agent/conftest v0.53.0/go.mod h1:ABFOf9kIX6cvP4lCa9vwCVaXaEw23aRKL+BXR0Lr7DM= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= +oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= diff --git a/gather/oci/internal/network/network.go b/gather/oci/internal/network/network.go new file mode 100644 index 0000000..536d3a5 --- /dev/null +++ b/gather/oci/internal/network/network.go @@ -0,0 +1,45 @@ +package network + +import ( + "net" + "strings" +) + +/* This code is sourced from the open-policy-agent/conftest project. */ +func Hostname(ref string) string { + ref = strings.TrimPrefix(ref, "oci://") + + colon := strings.Index(ref, ":") + slash := strings.Index(ref, "/") + + cut := colon + if colon == -1 || (colon > slash && slash != -1) { + cut = slash + } + + if cut < 0 { + return ref + } + + return ref[0:cut] +} + +func IsLoopback(host string) bool { + if host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "0:0:0:0:0:0:0:1" { + // fast path + return true + } + + ips, err := net.LookupIP(host) + if err != nil { + return false + } + + for _, ip := range ips { + if ip.IsLoopback() { + return true + } + } + + return false +} \ No newline at end of file diff --git a/gather/oci/internal/registry/client.go b/gather/oci/internal/registry/client.go new file mode 100644 index 0000000..d548b7b --- /dev/null +++ b/gather/oci/internal/registry/client.go @@ -0,0 +1,46 @@ +package registry + +import ( + "net/http" + + "github.com/enterprise-contract/go-gather/gather/oci/internal/network" + "github.com/spf13/viper" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/credentials" + "oras.land/oras-go/v2/registry/remote/retry" +) + +func SetupClient(repository *remote.Repository) error { + registry := repository.Reference.Host() + + // If `--tls=false` was provided or accessing the registry via loopback with + // `--tls` flag was not provided + if !viper.GetBool("tls") || (network.IsLoopback(network.Hostname(registry)) && !viper.IsSet("tls")) { + // Docker by default accesses localhost using plaintext HTTP + repository.PlainHTTP = true + } + + httpClient := &http.Client{ + Transport: retry.NewTransport(http.DefaultTransport), + } + + store, err := credentials.NewStoreFromDocker(credentials.StoreOptions{ + AllowPlaintextPut: true, + DetectDefaultNativeStore: true, + }) + if err != nil { + return err + } + + client := &auth.Client{ + Client: httpClient, + Credential: credentials.Credential(store), + Cache: auth.NewCache(), + } + client.SetUserAgent("conftest") + + repository.Client = client + + return nil +} \ No newline at end of file diff --git a/gather/oci/oci.go b/gather/oci/oci.go new file mode 100644 index 0000000..3126044 --- /dev/null +++ b/gather/oci/oci.go @@ -0,0 +1,83 @@ +package oci + +import ( + "context" + "fmt" + "os" + "strings" + + r "github.com/enterprise-contract/go-gather/gather/oci/internal/registry" + + "github.com/enterprise-contract/go-gather/metadata" + "github.com/enterprise-contract/go-gather/metadata/oci" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content/file" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" +) + +// OCIGatherer is a struct that implements the Gatherer interface +// and provides methods for gathering from OCI. +type OCIGatherer struct{} + +// Gather copies a file or directory from the source path to the destination path. +// It returns the metadata of the gathered file or directory and any error encountered. +// Portions of this file are derivative from the open-policy-agent/conftest project. +func (f *OCIGatherer) Gather(ctx context.Context, source, destination string) (metadata.Metadata, error) { + // Parse the source URI + repo := ociURLParse(source) + + // Get the artifact reference + ref, err := registry.ParseReference(repo) + if err != nil { + return nil, fmt.Errorf("failed to parse reference: %w", err) + } + + // If the reference is empty, set it to "latest" + if ref.Reference == "" { + ref.Reference = "latest" + repo = ref.String() + } + + // Create the repository client + src, err := remote.NewRepository(repo) + if err != nil { + return nil, fmt.Errorf("failed to create repository client: %w", err) + } + + // Setup the client for the repository + if err := r.SetupClient(src); err != nil { + return nil, fmt.Errorf("failed to setup repository client: %w", err) + } + + // Create the destination directory + if err := os.MkdirAll(destination, os.ModePerm); err != nil { + return nil, fmt.Errorf("failed to create directory: %w", err) + } + + // Create the file store + fileStore, err := file.New(destination) + if err != nil { + return nil, fmt.Errorf("file store: %w", err) + } + defer fileStore.Close() + + // Copy the artifact to the file store + a, err := oras.Copy(ctx, src, repo, fileStore, "", oras.DefaultCopyOptions) + if err != nil { + return nil, fmt.Errorf("pulling policy: %w", err) + } + + m := &oci.OCIMetadata{ + Digest: a.Digest.String(), + } + return m, nil +} + +func ociURLParse(source string) string { + scheme, src, found := strings.Cut(source, "://") + if !found { + src = scheme + } + return src +} \ No newline at end of file diff --git a/gather/oci/oci_test.go b/gather/oci/oci_test.go new file mode 100644 index 0000000..22d25e6 --- /dev/null +++ b/gather/oci/oci_test.go @@ -0,0 +1,198 @@ +package oci + +import ( + "context" + "fmt" + "os" + "strings" + "testing" +) + +func getRegistryURL(src string) string { + parts := strings.Split(src, "/") + lastPart := parts[len(parts)-1] + if strings.Contains(lastPart, ":") { + return src + } + return src + ":latest" +} + +// TestGetRegistryURL tests the getRegistryURL function. +func TestGetRegistryURL(t *testing.T) { + testCases := []struct { + src string + expected string + }{ + {src: "docker.io/library/alpine", expected: "docker.io/library/alpine:latest"}, + {src: "docker.io/library/alpine:3.12", expected: "docker.io/library/alpine:3.12"}, + {src: "https://docker.io/library/alpine", expected: "https://docker.io/library/alpine:latest"}, + {src: "alpine", expected: "alpine:latest"}, + } + + for _, tc := range testCases { + actual := getRegistryURL(tc.src) + if actual != tc.expected { + t.Errorf("Expected getRegistryURL(%s) to return %s, but got %s", tc.src, tc.expected, actual) + } + } +} + +func TestOCIURLParse(t *testing.T) { + testCases := []struct { + source string + expected string + }{ + {source: "docker.io/library/alpine", expected: "docker.io/library/alpine"}, + {source: "https://docker.io/library/alpine", expected: "docker.io/library/alpine"}, + {source: "alpine", expected: "alpine"}, + {source: "https://example.com/image:tag", expected: "example.com/image:tag"}, + } + + for _, tc := range testCases { + actual := ociURLParse(tc.source) + if actual != tc.expected { + t.Errorf("Expected ociURLParse(%s) to return %s, but got %s", tc.source, tc.expected, actual) + } + } +} + +// TestOCIGatherer_Gather tests the Gather function. +func TestOCIGatherer_Gather(t *testing.T) { + ctx := context.TODO() + + testCases := []struct { + name string + source string + destination string + expectedRepo string + expectedErr error + }{ + { + name: "Valid source URI", + source: "quay.io/libpod/alpine", + destination: "/tmp/foo", + expectedRepo: "quay.io/libpod/alpine:latest", + expectedErr: nil, + }, + { + name: "Valid source URI with tag", + source: "quay.io/libpod/alpine:3.2", + destination: "/tmp/foo", + expectedRepo: "quay.io/libpod/alpine:3.2", + expectedErr: nil, + }, + { + name: "Valid source URI with HTTPS", + source: "https://quay.io/libpod/alpine", + destination: "/tmp/foo", + expectedRepo: "https://quay.io/libpod/alpine:latest", + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gatherer := &OCIGatherer{} + metadata, err := gatherer.Gather(ctx, tc.source, tc.destination) + + if err != tc.expectedErr { + t.Errorf("Expected error: %v, but got: %v", tc.expectedErr, err) + } + + if metadata != nil { + t.Errorf("Expected metadata to be nil, but got: %v", metadata) + } + }) + t.Cleanup(func() { + // Cleanup the destination directory + os.RemoveAll(tc.destination) + }) + } +} + +// TestOCIGatherer_Gather_Invalid_URIs tests the Gather function with invalid source URIs. +func TestOCIGatherer_Gather_Invalid_URIs(t *testing.T) { + ctx := context.TODO() + + testCases := []struct { + name string + source string + destination string + expectedErr error + }{ + { + name: "Invalid source URI", + source: "invalid", + destination: "/tmp/foo", + expectedErr: fmt.Errorf("failed to parse reference: invalid reference: missing registry or repository"), + }, + { + name: "Invalid source URI with tag", + source: "invalid:tag", + destination: "/tmp/foo", + expectedErr: fmt.Errorf("failed to parse reference: invalid reference: missing registry or repository"), + }, + { + name: "Invalid source URI with HTTPS", + source: "https://invalid", + destination: "/tmp/foo", + expectedErr: fmt.Errorf("failed to parse reference: invalid reference: missing registry or repository"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gatherer := &OCIGatherer{} + metadata, err := gatherer.Gather(ctx, tc.source, tc.destination) + + if err.Error() != tc.expectedErr.Error() { + t.Errorf("Expected error: %v, but got: %v", tc.expectedErr, err) + } + + if metadata != nil { + t.Errorf("Expected metadata to be nil, but got: %v", metadata) + } + }) + t.Cleanup(func() { + // Cleanup the destination directory + os.RemoveAll(tc.destination) + }) + } +} + +// TestOCIGatherer_Gather_ErorrCreatingNewRepository tests the Gather function with an error creating a new repository client. +func TestOCIGatherer_Gather_ErorrCreatingNewRepository(t *testing.T) { + testCases := []struct { + name string + source string + destination string + expectedErr error + }{ + { + name: "Error creating new repository", + source: "docker.io", + destination: "/tmp/foo", + expectedErr: fmt.Errorf("failed to parse reference: invalid reference: missing registry or repository"), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.TODO() + gatherer := &OCIGatherer{} + metadata, err := gatherer.Gather(ctx, tc.source, tc.destination) + + if err.Error() != tc.expectedErr.Error() { + t.Errorf("Expected error: %v, but got: %v", tc.expectedErr, err) + } + + if metadata != nil { + t.Errorf("Expected metadata to be nil, but got: %v", metadata) + } + }) + t.Cleanup(func() { + // Cleanup the destination directory + os.RemoveAll(tc.destination) + }) + } + +} diff --git a/metadata/oci/go.mod b/metadata/oci/go.mod new file mode 100644 index 0000000..ed128f7 --- /dev/null +++ b/metadata/oci/go.mod @@ -0,0 +1,3 @@ +module github.com/enterprise-contract/go-gather/metadata/oci + +go 1.21.9 diff --git a/metadata/oci/oci.go b/metadata/oci/oci.go new file mode 100644 index 0000000..bcc0167 --- /dev/null +++ b/metadata/oci/oci.go @@ -0,0 +1,11 @@ +package oci + +type OCIMetadata struct { + Digest string +} + +func (o OCIMetadata) Get() map[string]any { + return map[string]any{ + "digest": o.Digest, + } +}