diff --git a/go.mod b/go.mod index 5339efd..8227bb3 100644 --- a/go.mod +++ b/go.mod @@ -13,5 +13,9 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sync v0.6.0 // indirect + oras.land/oras-go/v2 v2.5.0 // indirect ) diff --git a/go.sum b/go.sum index 44a92f4..5ba1d73 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,21 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= 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= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/uriget/example_test.go b/uriget/example_test.go index ae490df..f4ad784 100644 --- a/uriget/example_test.go +++ b/uriget/example_test.go @@ -51,3 +51,38 @@ func ExampleWithHttpClient() { fmt.Println(err) // Output: failed to make get request: Get "https://example.com": no proxy } +func ExampleGetFile_oci() { + testUrl := "oci://ghcr.io/score-spec/score-compose:0.18.0" + buff, err := GetFile(context.Background(), testUrl) + if err != nil { + fmt.Println("failed to pull OCI image:", err) + return + } + fmt.Println(len(buff) > 0) + // Output: + // true +} + +func ExampleGetFile_ociNoTag() { + testUrl := "oci://ghcr.io/score-spec/score-compose" + buff, err := GetFile(context.Background(), testUrl) + if err != nil { + fmt.Println("failed to pull OCI image:", err) + return + } + fmt.Println(len(buff) > 0) + // Output: + // true +} + +func ExampleGetFile_ociWithDigest() { + testUrl := "oci://ghcr.io/score-spec/score-compose@sha256:f3d8d5485a751cbdc91e073df1b6fbcde83f85a86ee3bc7d53e05b00452baedd" + buff, err := GetFile(context.Background(), testUrl) + if err != nil { + fmt.Println("failed to pull OCI image:", err) + return + } + fmt.Println(len(buff) > 0) + // Output: + // true +} diff --git a/uriget/uriget.go b/uriget/uriget.go index 4639d69..2d3a772 100644 --- a/uriget/uriget.go +++ b/uriget/uriget.go @@ -16,6 +16,11 @@ import ( "path/filepath" "strings" "time" + + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content/oci" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" ) // options is a struct holding fields that may need to have overrides in certain environments or during unit testing. @@ -110,6 +115,8 @@ func GetFile(ctx context.Context, rawUri string, optionFuncs ...Option) ([]byte, fallthrough case "git-https": return opts.getGit(ctx, u) + case "oci": + return opts.getOci(ctx, u) default: return nil, fmt.Errorf("unsupported scheme '%s'", u.Scheme) } @@ -233,3 +240,29 @@ func (o *options) getGit(ctx context.Context, u *url.URL) ([]byte, error) { o.logger.Printf("Read %d bytes from %s", len(buff), filepath.Join(td, subPath)) return buff, nil } + +func (o *options) getOci(ctx context.Context, u *url.URL) ([]byte, error) { + ref, err := registry.ParseReference(u.Host + u.Path) + if err != nil { + return nil, fmt.Errorf("can't parse artifact URL into a valid reference: %w", err) + } + store, err := oci.New(o.tempDir) + if err != nil { + return nil, fmt.Errorf("failed to create OCI layout store: %w", err) + } + remoteRepo, err := remote.NewRepository(ref.String()) + if err != nil { + return nil, fmt.Errorf("failed to connect to remote repository: %w", err) + } + tag := "latest" + if ref.Reference != "" { + tag = ref.Reference + } + manifestDescriptor, err := oras.Copy(ctx, remoteRepo, tag, store, tag, oras.DefaultCopyOptions) + if err != nil { + return nil, fmt.Errorf("failed to pull OCI image: %w", err) + } + + o.logger.Printf("Pulled OCI image: %s with manifest descriptor: %v", u.String(), manifestDescriptor.Digest) + return []byte(manifestDescriptor.Digest), nil +}